Hallo Habr!
Dieser Beitrag beginnt mit der Analyse von Aufgaben nach den Algorithmen, die groĂe IT-Unternehmen (Mail.Ru Group, Google usw.) gerne Kandidaten fĂŒr Interviews geben (wenn das Interview mit Algorithmen nicht gut ist, dann die Chancen, einen Job in einem Traumunternehmen zu bekommen, leider gegen Null tendieren). Erstens ist dieser Beitrag nĂŒtzlich fĂŒr diejenigen, die keine Erfahrung mit Olympiadenprogrammierung oder schweren Kursen wie ShAD oder LKS haben, in denen die Themen der Algorithmen ernst genug genommen werden, oder fĂŒr diejenigen, die ihr Wissen in einem bestimmten Bereich auffrischen möchten.
Gleichzeitig kann nicht argumentiert werden, dass alle Aufgaben, die hier behandelt werden, sicherlich im Interview erfĂŒllt werden. Die AnsĂ€tze, mit denen solche Aufgaben gelöst werden, sind jedoch in den meisten FĂ€llen Ă€hnlich.

Die ErzÀhlung wird in verschiedene Themen unterteilt, und wir werden zunÀchst Mengen mit einer bestimmten Struktur generieren.
1. Beginnen wir mit dem Tastenakkordeon: Sie mĂŒssen alle korrekten Klammersequenzen mit Klammern des gleichen Typs ( was ist die richtige Klammerfolge) generieren, wobei die Anzahl der Klammern k ist.
Dieses Problem kann auf verschiedene Arten gelöst werden. Beginnen wir mit rekursiv .
Diese Methode setzt voraus, dass wir beginnen, Sequenzen aus einer leeren Liste zu durchlaufen. Nachdem die Klammer (Ăffnen oder SchlieĂen) zur Liste hinzugefĂŒgt wurde, wird der Rekursionsaufruf erneut ausgefĂŒhrt und die Bedingungen werden ĂŒberprĂŒft. Wie sind die Bedingungen? Es ist notwendig, den Unterschied zwischen öffnenden und schlieĂenden Klammern (Variable cnt
) zu ĂŒberwachen. Sie können der Liste keine schlieĂende Klammer hinzufĂŒgen, wenn dieser Unterschied nicht positiv ist, da sonst die Klammerfolge nicht mehr korrekt ist. Es bleibt, dies sorgfĂ€ltig im Code zu implementieren.
k = 6 # init = list(np.zeros(k)) # , cnt = 0 # ind = 0 # , def f(cnt, ind, k, init): # . , if (cnt <= k-ind-2): init[ind] = '(' f(cnt+1, ind+1, k, init) # . , cnt > 0 if cnt > 0: init[ind] = ')' f(cnt-1, ind+1, k, init) # if ind == k: if cnt == 0: print (init)
Die KomplexitÀt dieses Algorithmus ist zusÀtzlicher Speicher erforderlich .
Mit den angegebenen Parametern wird der Funktionsaufruf gibt Folgendes aus:

Ein iterativer Weg, um dieses Problem zu lösen: In diesem Fall wird die Idee grundlegend anders sein - Sie mĂŒssen das Konzept der lexikografischen Reihenfolge fĂŒr Klammersequenzen einfĂŒhren.
Alle korrekten Klammersequenzen fĂŒr einen Klammertyp können unter BerĂŒcksichtigung der Tatsache geordnet werden, dass . Zum Beispiel ist fĂŒr n = 6 die lexikographisch niedrigste Sequenz und der Ă€lteste - .
Um die nĂ€chste lexikografische Sequenz zu erhalten, mĂŒssen Sie die am weitesten rechts stehende öffnende Klammer finden, vor der sich eine schlieĂende Klammer befindet, damit die Klammersequenz beim Vertauschen an bestimmten Stellen korrekt bleibt. Wir tauschen sie aus und machen das Suffix zum lexikografisch kleinsten - dafĂŒr mĂŒssen Sie bei jedem Schritt die Differenz zwischen der Anzahl der Klammern berechnen.
Meiner Meinung nach ist dieser Ansatz etwas trostlos als rekursiv, aber er kann verwendet werden, um andere Probleme bei der Erzeugung von Sets zu lösen. Wir implementieren dies im Code.
# , n = 6 arr = ['(' for _ in range(n//2)] + [')' for _ in range(n//2)] def f(n, arr): # print (arr) while True: ind = n-1 cnt = 0 # . , while ind>=0: if arr[ind] == ')': cnt -= 1 if arr[ind] == '(': cnt += 1 if cnt < 0 and arr[ind] =='(': break ind -= 1 # , if ind < 0: break # . arr[ind] = ')' # for i in range(ind+1,n): if i <= (n-ind+cnt)/2 +ind: arr[i] = '(' else: arr[i] = ')' print (arr)
Die KomplexitÀt dieses Algorithmus ist dieselbe wie im vorherigen Beispiel.
Ăbrigens gibt es eine einfache Methode, die zeigt, dass die Anzahl der generierten Klammersequenzen fĂŒr eine bestimmte n / 2 mit den katalanischen Zahlen ĂŒbereinstimmen muss.

Es funktioniert!
GroĂartig, mit den Klammern fĂŒr den Moment, lassen Sie uns fortfahren, Teilmengen zu generieren. Beginnen wir mit einem einfachen RĂ€tsel.
2. Gegeben ein Array aufsteigender Reihenfolge mit Zahlen von vorher Es ist erforderlich, alle seine Teilmengen zu generieren.
Beachten Sie, dass die Anzahl der Teilmengen einer solchen Menge genau ist . Wenn jede Teilmenge als Array von Indizes dargestellt wird, wo bedeutet, dass das Element nicht in der Menge enthalten ist, aber - Was enthalten ist, dann wird die Erzeugung aller dieser Arrays die Erzeugung aller Teilmengen sein.
Wenn wir die bitweise Darstellung von Zahlen von 0 bis betrachten Dann geben sie die gewĂŒnschten Teilmengen an. Das heiĂt, um das Problem zu lösen, ist es notwendig, die Addition von Eins zu einer BinĂ€rzahl zu implementieren. Wir beginnen mit allen Nullen und enden mit einem Array, in dem es eine Einheit gibt.
n = 3 B = np.zeros(n+1) # B 1 ( ) a = np.array(list(range(n))) # def f(B, n, a): # while B[0] == 0: ind = n # while B[ind] == 1: ind -= 1 # 1 B[ind] = 1 # B[(ind+1):] = 0 print (a[B[1:].astype('bool')])
Die KomplexitÀt dieses Algorithmus ist aus dem GedÀchtnis - .

Lassen Sie uns nun die vorherige Aufgabe etwas komplizieren.
3. Geben Sie ein geordnetes Array in aufsteigender Reihenfolge mit Zahlen von an vorher mĂŒssen Sie alles generieren -element Teilmengen (iterativ gelöst).
Ich stelle fest, dass die Formulierung dieses Problems der vorherigen Ă€hnlich ist und mit ungefĂ€hr der gleichen Methode gelöst werden kann: Nehmen Sie das ursprĂŒngliche Array mit Einheiten und Nullen und sortieren nacheinander alle Varianten solcher LĂ€ngenfolgen
Es sind jedoch geringfĂŒgige Ănderungen erforderlich. Wir mĂŒssen alles sortieren -bewertete SĂ€tze von Zahlen aus vorher . Sie mĂŒssen genau verstehen, wie Sie die Teilmengen sortieren mĂŒssen. In diesem Fall können wir das Konzept der lexikografischen Reihenfolge fĂŒr solche Mengen einfĂŒhren.
Wir ordnen die Reihenfolge auch nach Zeichencodes: (Das ist natĂŒrlich seltsam, aber es ist notwendig, und jetzt werden wir verstehen, warum). Zum Beispiel fĂŒr Das jĂŒngste und Ă€lteste wird die Sequenz sein und .
Es bleibt zu verstehen, wie der Ăbergang von einer Sequenz zur anderen beschrieben werden kann. Hier ist alles einfach: wenn wir uns Ă€ndern auf , dann schreiben wir links das lexikographisch minimale unter BerĂŒcksichtigung der Erhaltung des Zustands auf . Code:
n = 5 k = 3 a = np.array(list(range(n))) # k - init = [1 for _ in range(k)] + [0 for _ in range(nk)] def f(a, n, k, init): # k- print (a[a[init].astype('bool')]) while True: unit_cnt = 0 cur_ind = 0 # , 1 while (cur_ind < n) and (init[cur_ind]==1 or unit_cnt==0): if init[cur_ind] == 1: unit_cnt += 1 cur_ind += 1 # - if cur_ind == n: break # init[cur_ind] = 1 # . . for i in range(cur_ind): if i < unit_cnt-1: init[i] = 1 else: init[i] = 0 print (a[a[init].astype('bool')])
Arbeitsbeispiel:

Die KomplexitÀt dieses Algorithmus ist aus dem GedÀchtnis - .
4. Gegeben ein Array aufsteigender Reihenfolge mit Zahlen von vorher mĂŒssen Sie alles generieren -element Teilmengen (rekursiv lösen).
Versuchen wir nun die Rekursion. Die Idee ist einfach: Diesmal ohne Beschreibung, siehe Code.
n = 5 a = np.array(list(range(n))) ind = 0 # , num = 0 # , k = 3 sub = list(-np.ones(k)) # def f(n, a, num, ind, k, sub): # k , if ind == k: print (sub) else: for i in range(n - num): # , k if (n - num - i >= k - ind): # sub[ind] = a[num + i] # f(n, a, num+1+i, ind+1, k, sub)
Arbeitsbeispiel:

Die KomplexitÀt ist dieselbe wie bei der vorherigen Methode.
5. Gegeben ein Array aufsteigender Reihenfolge mit Zahlen von vorher ist es erforderlich, alle seine Permutationen zu erzeugen.
Wir werden mit Rekursion lösen. Die Lösung Àhnelt der vorherigen, bei der wir eine Hilfsliste haben. Es ist anfangs Null, wenn eingeschaltet -Die Stelle des Elements ist eine Einheit, dann das Element schon in der permutation. Kaum gesagt als getan:
a = np.array(range(3)) n = a.shape[0] ind_mark = np.zeros(n) # perm = -np.ones(n) # def f(ind_mark, perm, ind, n): if ind == n: print (perm) else: for i in range(n): if not ind_mark[i]: # ind_mark[i] = 1 # perm[ind] = i f(ind_mark, perm, ind+1, n) # - ind_mark[i] = 0
Arbeitsbeispiel:

Die KomplexitÀt dieses Algorithmus ist aus dem GedÀchtnis -
Betrachten Sie nun zwei sehr interessante RĂ€tsel fĂŒr Gray-Codes . Kurz gesagt, dies ist eine Reihe von Sequenzen gleicher LĂ€nge, wobei sich jede Sequenz in einer Kategorie von ihren Nachbarn unterscheidet.
6. Generieren Sie alle zweidimensionalen Gray-Codes der LĂ€nge n.
Die Idee, dieses Problem zu lösen, ist cool, aber wenn Sie die Lösung nicht kennen, kann es sehr schwierig sein, daran zu denken. Ich stelle fest, dass die Anzahl solcher Sequenzen ist .
Wir werden iterativ lösen. Angenommen, wir haben einen Teil solcher Sequenzen generiert und sie liegen in einer Liste. Wenn wir eine solche Liste duplizieren und in umgekehrter Reihenfolge schreiben, stimmt die letzte Sequenz in der ersten Liste mit der ersten Sequenz in der zweiten Liste ĂŒberein, die vorletzte mit der zweiten usw. Kombinieren Sie diese Listen zu einer.
Was muss getan werden, damit sich alle Sequenzen in der Liste in derselben Kategorie voneinander unterscheiden? Platzieren Sie eine Einheit an den entsprechenden Stellen in den Elementen der zweiten Liste, um die Gray-Codes zu erhalten.
Es ist schwierig, "nach Gehör" wahrzunehmen, wir werden Iterationen dieses Algorithmus darstellen.

n = 3 init = [list(np.zeros(n))] def f(n, init): for i in range(n): for j in range(2**i): init.append(list(init[2**i - j - 1])) init[-1][i] = 1.0 return init
Die KomplexitÀt dieser Aufgabe ist aus dem GedÀchtnis das gleiche.
Lassen Sie uns nun die Aufgabe komplizieren.
7. Generieren Sie alle k-dimensionalen Gray-Codes der LĂ€nge n.
Es ist klar, dass die vergangene Aufgabe nur ein Sonderfall dieser Aufgabe ist. Auf diese schöne Weise, wie die vergangene Aufgabe gelöst wurde, kann dies jedoch nicht gelöst werden. Hier ist es notwendig, die Sequenzen in der richtigen Reihenfolge zu sortieren. Lassen Sie uns ein zweidimensionales Array erhalten . Anfangs hat die letzte Zeile Einheiten und der Rest hat Nullen. DarĂŒber hinaus ist in der Matrix . Hier und unterscheiden sich in der Richtung: zeigt nach oben zeigt nach unten. Wichtig: In jeder Spalte kann es jederzeit nur eine geben oder und die restlichen Zahlen sind Nullen.

Jetzt werden wir verstehen, wie es möglich ist, diese Sequenzen zu sortieren, um Gray-Codes zu erhalten. Beginnen Sie am Ende, um das Element nach oben zu bewegen.

Im nĂ€chsten Schritt stoĂen wir an die Decke. Wir schreiben die resultierende Sequenz. Nachdem Sie das Limit erreicht haben, mĂŒssen Sie mit der nĂ€chsten Spalte beginnen. Sie mĂŒssen von rechts nach links danach suchen. Wenn wir eine Spalte finden, die verschoben werden kann, Ă€ndern sich die Pfeile fĂŒr alle Spalten, mit denen wir nicht arbeiten konnten, in die entgegengesetzte Richtung, damit sie wieder verschoben werden können.

Jetzt können Sie die erste Spalte nach unten verschieben. Wir bewegen es, bis es auf den Boden fĂ€llt. Und so weiter, bis alle Pfeile den Boden oder die Decke berĂŒhren und keine SĂ€ulen mehr ĂŒbrig sind, die bewegt werden können.
Im Rahmen der Speichereinsparung werden wir dieses Problem jedoch mit zwei eindimensionalen Arrays mit einer LÀnge lösen : In einem Array liegen die Elemente der Sequenz selbst und in der anderen Richtung (Pfeile).
n,k = 3,3 arr = np.zeros(n) direction = np.ones(n) # , def k_dim_gray(n,k): # print (arr) while True: ind = n-1 while ind >= 0: # , if (arr[ind] == 0 and direction[ind] == 0) or (arr[ind] == k-1 and direction[ind] == 1): direction[ind] = (direction[ind]+1)%2 else: break ind -= 1 # , if ind < 0: break # 1 , 1 arr[ind] += direction[ind]*2 - 1 print (arr)
Arbeitsbeispiel:

Die KomplexitÀt des Algorithmus ist aus dem GedÀchtnis - .
Die Richtigkeit dieses Algorithmus wird durch Induktion am bewiesen , hier werde ich die Beweise nicht beschreiben.
Im nÀchsten Beitrag werden wir die Aufgaben auf Dynamik analysieren.