Hallo!
Kürzlich habe ich Probleme aus dem
Timus Online Judge- Archiv gelöst und
bin auf einen Abschnitt namens
Dynamic Programming Tasks gestoßen . Diese Art von Aufgabe ist für mich von besonderem Interesse, da dieser Ansatz häufig die Geschwindigkeit und Eleganz der Lösung gewährleistet. Was ist dynamische Programmierung?
Dynamische Programmierung ist ein Ansatz zur Lösung von Problemen, bei denen eine Unterteilung in Teilaufgaben erfolgt, die im Vergleich zur ursprünglichen „einfacher“ sind. Das Wort "dynamisch" hat eine ähnliche Bedeutung wie "induktiv": Es wird angenommen, dass die Antwort für eine bestimmte Bedeutung bekannt ist
und ich möchte die Antwort finden für
. In der Mathematik wird dies als induktiver Übergang bezeichnet und ist die Hauptidee der dynamischen Programmierung.
Einfache Beispiele
Die auffälligste und indikativste Aufgabe ist die Aufgabe des Rechnens
Nummer der Fibonacci-Sequenz.
Es ist bekannt, dass die Sequenz die folgenden Eigenschaften hat:
Dies impliziert sofort die Wiederholungsformel:
int Fibonacci(int n) { if(n == 1 || n == 2) return 1; return Fibonacci(n-1) + Fibonacci(n-2); }
Wenn die Rekursion nach einer Zahl "vom Ende" sucht, berechnet die folgende Methode nacheinander alle dazwischen liegenden Zahlen
und
::
int dpFibonacci(int n) { int prev1 = 1; int prev2 = 1; int curr = 0; for(int j = 2; j < n; j++) { curr = prev1 + prev2; prev1 = prev2; prev2 = curr; } return curr; }
Es ist klar, dass für ausreichend große
Dieser Algorithmus arbeitet viel schneller: Er berechnet keine Zwischenwerte mehrmals. Betrachten Sie ein etwas komplexeres Beispiel.
Beispiel 1. Sie gehen auf einer mautpflichtigen Treppe. Auftreten
Schritt müssen Sie bezahlen
Münzen. Sie können zum nächsten Schritt übergehen oder über einen springen. Aufgabe: bestehen
Schritte und geben Sie so wenig Münzen wie möglich aus.
Es ist klar, dass wir bei jedem Schritt die Anzahl der „Zahlungen“ minimieren, aber wir können auf einen sehr teuren Schritt stoßen, den wir vermeiden möchten. Erstellen Sie ein Array von Werten
in dem auf
-Der Platz ist die (minimale) Anzahl von Münzen, die ausgegeben werden müssen, um dorthin zu gelangen
th Schritt. Es ist sofort klar, dass
. Und dann werden wir mindestens die beiden vorherigen Schritte ausführen und die Kosten für den Schritt selbst addieren:
Wir ändern die Bedingungen des Problems ein wenig: Nehmen wir an, dass Sie in einigen Schritten Münzen erhalten können (dies bedeutet das
) Was muss im Algorithmus geändert werden, damit das richtige Ergebnis erzielt wird?
LösungEs ist nur notwendig, den „Anfang“ unserer Dynamik zu ändern. Wenn die erste Treppe uns keine Münzen bringt, ist es jedoch ratsam, darüber zu springen, wenn Es ist besser, Ihre Münzen zu sammeln. Also .
Stellen Sie sich ein anderes Beispiel vor, das eine „zweidimensionale“ Dynamik verwendet.
Beispiel 2. Im Labyrinth gibt es
Räume, von denen jeder Gold enthält (in einem Käfig
Lügen
Gold). Die Aufgabe besteht darin zu bestimmen, welche maximale Menge Gold mit einer optimalen Route von einem Punkt aus gesammelt werden kann
auf den Punkt
wenn Sie entweder nach unten oder rechts gehen können.
Wir wollen also den besten Weg zur Zelle kennen
. Wir können aus zwei Zellen hierher kommen -
und
. Vorausgesetzt, die optimalen Routen für diese beiden Zellen sind bekannt (sie sind in einer Tabelle gespeichert
), dann die Antwort für die Zelle
erhalten wie folgt:
Dies ist eine weitere klassische dynamische Programmieraufgabe, deren Modifikationen bei Sportprogrammieraufgaben häufig vorkommen. Eine ähnliche Aufgabe wird
hier näher erläutert.
Anspruchsvollere Aufgaben
Auf Wunsch kann ein dynamischer Ansatz beliebig angeschraubt werden. Betrachten Sie eine
Aufgabe aus dem Timus Online Judge-Archiv.
Die mathematische Formulierung des Problems lautet wie folgt: Es ist erforderlich, die Mindestanzahl von Begriffen zu finden, die erforderlich sind, um eine bestimmte Anzahl in vollständige Quadrate zu zerlegen.
Nehmen wir an, wir kennen nach wie vor die Antworten für alle Zahlen
die in einem Array gespeichert sind
und wir würden gerne finden
.
Nimm diese Nummer
und analysieren, welche Situationen sein können:
- ist ein volles Quadrat. In diesem Fall .
- Vielleicht die vorherige Nummer war ein komplettes Quadrat. Dann .
Im Allgemeinen scheint die Option, eine Einheit zur vorherigen hinzuzufügen, nicht so schlecht zu sein.
Wir gehen wie folgt vor: Wir suchen eine Zersetzung
so dass
Als
- dann volles Quadrat
und
Das heißt, wir haben eine Partition gefunden, die einfach besser ist als
und die Antwort in diesem Fall wird sein
Beispiel für Java-Code, der diesen Algorithmus implementiert: for(int k = 1; k <= n; k++) { int best = d[k - 1] + 1;
Betrachten Sie das folgende
Problem . Das Ziel ist es, eine Treppe aus zu bauen
Würfel nach den Regeln:
- Die Treppe hat mindestens zwei Stufen.
- Eine Treppe kann nicht zwei identische Stufen haben.
- Die Stufen der Treppe verlaufen in aufsteigender Reihenfolge (dh die nächste ist größer als die vorherige).
Dieses Mal werden wir eine zweidimensionale Dynamik aufbauen. Erstellen Sie eine Tabelle
in der die Position
die Anzahl der Treppen bestehend aus
Würfel, deren Höhe nicht überschreitet
. Wenn es klappt, ist die Antwort auf unser Problem die Summe
Wir werden also das Problem lösen, die Anzahl der Treppen zu ermitteln, aus denen sich die Treppe zusammensetzt
Würfel, die groß sind
. Das Bild zeigt die Treppen, die hineinfallen
::

Da wir alle Treppen kennen, die aus weniger Würfeln bestehen, werden wir die Treppen „abspalten“
rechte Spalte. Das Ergebnis ist eine Treppe c
Würfel. Beispiel für
::

Aber für solche Treppen ist das Ergebnis bereits bekannt, so dass wir alle diese Treppen mit einem Zyklus durch sortieren werden
und addieren Sie alle Ergebnisse. Auf diese Weise,
Jetzt werden wir die Höhen der Treppen sortieren:
Endlich ändern
von
vorher
Wir bekommen die Antwort.
Wichtig : Bei der Erstellung unserer Matrix ist dies zu berücksichtigen
, da sonst einige Arten von Treppen „verloren gehen“ (wenn sie „abgespalten“ werden), aber es versteht sich von selbst, dass eine solche Treppe die Bedingungen des Problems nicht erfüllt, lautet die Antwort die Nummer
.
Beispiel für Java-Code, der diesen Algorithmus implementiert: dp = new long[n + 1][n+1]; d[1][1] = 1; d[2][1] = 0; d[2][2] = 1; for(int i = 3; i < n + 1; i++) { for(int j = 2; j <i; j++) { long cnt = 0; for(int k = 1; k < j; k++) { cnt += d[i - j][k]; } d[i][j] = cnt; } d[i][i] = 1;
Die nächste
Aufgabe wird mit einem eindimensionalen Array gelöst.
Also was wir haben. Der erste ent kennt 2 Wörter. Jeder ent lehrt alle Wörter, die er selbst kennt, zwei ents: jung und alt. Im Gegenzug wurden den Jungen so viele Wörter beigebracht, wie er bereits weiß, und den Alten wurde nur ein Wort beigebracht. Sie müssen wissen, wie viele Ents genau wissen
Wörter (es ist notwendig, die Anzahl dieser ents modulo abzuleiten
)
Die Lösung ist ganz einfach. Erstellen Sie ein Array
in dem auf
-th Platz speichern wir die Anzahl der ents (modulo
) wer weiß
Worte. Alles beginnt mit dem ersten Ent, der daher zwei Wörter kennt
. Und dann ist alles einfach:
- Alle Enten, die eine ungerade Anzahl von Wörtern kennen, sind alt und können nur von den vorherigen lernen. Deshalb für ungerade
- Was die Ents betrifft, die eine gerade Anzahl von Wörtern kennen, so sind dies alle diejenigen, die die gleiche Anzahl von Wörtern von Elfen (jung) erhalten haben. diejenigen, die aus dem Vorherigen gelernt haben (alt); das heißt, für gerade wir haben .
Es bleibt das Berechnungsmodulo zu behandeln. Um keine großen Zahlen zu speichern, werden wir uns sofort an alle modulo-Werte erinnern.
Beispiel für Java-Code, der diesen Algorithmus implementiert: int[] d = new int[K + 1]; if(K >= 2) d[2] = 1; if(P != 1) { for(int i = 3; i <= K; i++) { if(i % 2 != 0) { d[i] = d[i - 1]; } else { d[i] = ((d[i/2] % P) + d[i - 1] % P) % P; } } } else d[K] = 0;
Verwendete Ressourcen:
- Timus Online Richter;
- Ein bisschen über dynamische Programmierung;
- Vergleichseigenschaften modulo.