Interviewleitfaden zu Algorithmen und Datenstruktur – Detaillierte Erläuterung des Backtracking-Algorithmus (2)

Beschneidung

Komplexe Backtracking-Probleme enthalten häufig eine oder mehrere Einschränkungen, und Einschränkungen können häufig zum „Beschneiden“ verwendet werden .

!!! Frage „Beispielfrage drei“

在二叉树中搜索所有值为 $7$ 的节点,请返回根节点到这些节点的路径,**并要求路径中不包含值为 $3$ 的节点**。

Um die oben genannten Einschränkungen zu erfüllen, müssen wir eine Bereinigungsoperation hinzufügen : Wenn der Wert während des Suchvorgangs 3 3 ist3 Knoten, kehren Sie früher zurück und hören Sie auf zu suchen.

=== „Python“

```python title="preorder_traversal_iii_compact.py"
[class]{}-[func]{pre_order}
```

=== „C++“

```cpp title="preorder_traversal_iii_compact.cpp"
[class]{}-[func]{preOrder}
```

=== „Java“

```java title="preorder_traversal_iii_compact.java"
[class]{preorder_traversal_iii_compact}-[func]{preOrder}
```

=== „C#“

```csharp title="preorder_traversal_iii_compact.cs"
[class]{preorder_traversal_iii_compact}-[func]{preOrder}
```

=== „Los“

```go title="preorder_traversal_iii_compact.go"
[class]{}-[func]{preOrderIII}
```

=== „Schnell“

```swift title="preorder_traversal_iii_compact.swift"
[class]{}-[func]{preOrder}
```

=== „JS“

```javascript title="preorder_traversal_iii_compact.js"
[class]{}-[func]{preOrder}
```

=== „TS“

```typescript title="preorder_traversal_iii_compact.ts"
[class]{}-[func]{preOrder}
```

=== „Dart“

```dart title="preorder_traversal_iii_compact.dart"
[class]{}-[func]{preOrder}
```

=== „Rost“

```rust title="preorder_traversal_iii_compact.rs"
[class]{}-[func]{pre_order}
```

=== „C“

```c title="preorder_traversal_iii_compact.c"
[class]{}-[func]{preOrder}
```

=== „Zick“

```zig title="preorder_traversal_iii_compact.zig"
[class]{}-[func]{preOrder}
```

Beschneiden ist ein sehr anschaulicher Begriff. Wie in der folgenden Abbildung dargestellt, „schneiden“ wir während des Suchvorgangs Suchzweige ab, die die Einschränkungen nicht erfüllen, um viele sinnlose Versuche zu vermeiden und dadurch die Sucheffizienz zu verbessern.

Fügen Sie hier eine Bildbeschreibung ein

Framework-Code

Als nächstes versuchen wir, das Hauptgerüst des Backtrackings durch „Try, Rollback und Prune“ zu extrahieren, um die Vielseitigkeit des Codes zu verbessern.

Im folgenden Rahmencode statestellt er den aktuellen Status des Problems und choicesdie Auswahlmöglichkeiten dar, die im aktuellen Status getroffen werden können.

=== „Python“

```python title=""
def backtrack(state: State, choices: list[choice], res: list[state]):
    """回溯算法框架"""
    # 判断是否为解
    if is_solution(state):
        # 记录解
        record_solution(state, res)
        # 停止继续搜索
        return
    # 遍历所有选择
    for choice in choices:
        # 剪枝:判断选择是否合法
        if is_valid(state, choice):
            # 尝试:做出选择,更新状态
            make_choice(state, choice)
            backtrack(state, choices, res)
            # 回退:撤销选择,恢复到之前的状态
            undo_choice(state, choice)
```

=== „C++“

```cpp title=""
/* 回溯算法框架 */
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {
    // 判断是否为解
    if (isSolution(state)) {
        // 记录解
        recordSolution(state, res);
        // 停止继续搜索
        return;
    }
    // 遍历所有选择
    for (Choice choice : choices) {
        // 剪枝:判断选择是否合法
        if (isValid(state, choice)) {
            // 尝试:做出选择,更新状态
            makeChoice(state, choice);
            backtrack(state, choices, res);
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, choice);
        }
    }
}
```

=== „Java“

```java title=""
/* 回溯算法框架 */
void backtrack(State state, List<Choice> choices, List<State> res) {
    // 判断是否为解
    if (isSolution(state)) {
        // 记录解
        recordSolution(state, res);
        // 停止继续搜索
        return;
    }
    // 遍历所有选择
    for (Choice choice : choices) {
        // 剪枝:判断选择是否合法
        if (isValid(state, choice)) {
            // 尝试:做出选择,更新状态
            makeChoice(state, choice);
            backtrack(state, choices, res);
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, choice);
        }
    }
}
```

=== „C#“

```csharp title=""
/* 回溯算法框架 */
void backtrack(State state, List<Choice> choices, List<State> res) {
    // 判断是否为解
    if (isSolution(state)) {
        // 记录解
        recordSolution(state, res);
        // 停止继续搜索
        return;
    }
    // 遍历所有选择
    foreach (Choice choice in choices) {
        // 剪枝:判断选择是否合法
        if (isValid(state, choice)) {
            // 尝试:做出选择,更新状态
            makeChoice(state, choice);
            backtrack(state, choices, res);
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, choice);
        }
    }
}
```

=== „Los“

```go title=""
/* 回溯算法框架 */
func backtrack(state *State, choices []Choice, res *[]State) {
    // 判断是否为解
    if isSolution(state) {
        // 记录解
        recordSolution(state, res)
        // 停止继续搜索
        return
    }
    // 遍历所有选择
    for _, choice := range choices {
        // 剪枝:判断选择是否合法
        if isValid(state, choice) {
            // 尝试:做出选择,更新状态
            makeChoice(state, choice)
            backtrack(state, choices, res)
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, choice)
        }
    }
}
```

=== „Schnell“

```swift title=""
/* 回溯算法框架 */
func backtrack(state: inout State, choices: [Choice], res: inout [State]) {
    // 判断是否为解
    if isSolution(state: state) {
        // 记录解
        recordSolution(state: state, res: &res)
        // 停止继续搜索
        return
    }
    // 遍历所有选择
    for choice in choices {
        // 剪枝:判断选择是否合法
        if isValid(state: state, choice: choice) {
            // 尝试:做出选择,更新状态
            makeChoice(state: &state, choice: choice)
            backtrack(state: &state, choices: choices, res: &res)
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state: &state, choice: choice)
        }
    }
}
```

=== „JS“

```javascript title=""
/* 回溯算法框架 */
function backtrack(state, choices, res) {
    // 判断是否为解
    if (isSolution(state)) {
        // 记录解
        recordSolution(state, res);
        // 停止继续搜索
        return;
    }
    // 遍历所有选择
    for (let choice of choices) {
        // 剪枝:判断选择是否合法
        if (isValid(state, choice)) {
            // 尝试:做出选择,更新状态
            makeChoice(state, choice);
            backtrack(state, choices, res);
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, choice);
        }
    }
}
```

=== „TS“

```typescript title=""
/* 回溯算法框架 */
function backtrack(state: State, choices: Choice[], res: State[]): void {
    // 判断是否为解
    if (isSolution(state)) {
        // 记录解
        recordSolution(state, res);
        // 停止继续搜索
        return;
    }
    // 遍历所有选择
    for (let choice of choices) {
        // 剪枝:判断选择是否合法
        if (isValid(state, choice)) {
            // 尝试:做出选择,更新状态
            makeChoice(state, choice);
            backtrack(state, choices, res);
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, choice);
        }
    }
}
```

=== „Dart“

```dart title=""
/* 回溯算法框架 */
void backtrack(State state, List<Choice>, List<State> res) {
  // 判断是否为解
  if (isSolution(state)) {
    // 记录解
    recordSolution(state, res);
    // 停止继续搜索
    return;
  }
  // 遍历所有选择
  for (Choice choice in choices) {
    // 剪枝:判断选择是否合法
    if (isValid(state, choice)) {
      // 尝试:做出选择,更新状态
      makeChoice(state, choice);
      backtrack(state, choices, res);
      // 回退:撤销选择,恢复到之前的状态
      undoChoice(state, choice);
    }
  }
}
```

=== „Rost“

```rust title=""

```

=== „C“

```c title=""
/* 回溯算法框架 */
void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {
    // 判断是否为解
    if (isSolution(state)) {
        // 记录解
        recordSolution(state, res, numRes);
        // 停止继续搜索
        return;
    }
    // 遍历所有选择
    for (int i = 0; i < numChoices; i++) {
        // 剪枝:判断选择是否合法
        if (isValid(state, &choices[i])) {
            // 尝试:做出选择,更新状态
            makeChoice(state, &choices[i]);
            backtrack(state, choices, numChoices, res, numRes);
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, &choices[i]);
        }
    }
}
```

=== „Zick“

```zig title=""

```

Als nächstes lösen wir Beispielproblem 3 basierend auf dem Framework-Code. Der Zustand statedurchläuft den Pfad für den Knoten, wählt choicesden linken untergeordneten Knoten und den rechten untergeordneten Knoten des aktuellen Knotens aus und das Ergebnis resist eine Pfadliste.

=== „Python“

```python title="preorder_traversal_iii_template.py"
[class]{}-[func]{is_solution}

[class]{}-[func]{record_solution}

[class]{}-[func]{is_valid}

[class]{}-[func]{make_choice}

[class]{}-[func]{undo_choice}

[class]{}-[func]{backtrack}
```

=== „C++“

```cpp title="preorder_traversal_iii_template.cpp"
[class]{}-[func]{isSolution}

[class]{}-[func]{recordSolution}

[class]{}-[func]{isValid}

[class]{}-[func]{makeChoice}

[class]{}-[func]{undoChoice}

[class]{}-[func]{backtrack}
```

=== „Java“

```java title="preorder_traversal_iii_template.java"
[class]{preorder_traversal_iii_template}-[func]{isSolution}

[class]{preorder_traversal_iii_template}-[func]{recordSolution}

[class]{preorder_traversal_iii_template}-[func]{isValid}

[class]{preorder_traversal_iii_template}-[func]{makeChoice}

[class]{preorder_traversal_iii_template}-[func]{undoChoice}

[class]{preorder_traversal_iii_template}-[func]{backtrack}
```

=== „C#“

```csharp title="preorder_traversal_iii_template.cs"
[class]{preorder_traversal_iii_template}-[func]{isSolution}

[class]{preorder_traversal_iii_template}-[func]{recordSolution}

[class]{preorder_traversal_iii_template}-[func]{isValid}

[class]{preorder_traversal_iii_template}-[func]{makeChoice}

[class]{preorder_traversal_iii_template}-[func]{undoChoice}

[class]{preorder_traversal_iii_template}-[func]{backtrack}
```

=== „Los“

```go title="preorder_traversal_iii_template.go"
[class]{}-[func]{isSolution}

[class]{}-[func]{recordSolution}

[class]{}-[func]{isValid}

[class]{}-[func]{makeChoice}

[class]{}-[func]{undoChoice}

[class]{}-[func]{backtrackIII}
```

=== „Schnell“

```swift title="preorder_traversal_iii_template.swift"
[class]{}-[func]{isSolution}

[class]{}-[func]{recordSolution}

[class]{}-[func]{isValid}

[class]{}-[func]{makeChoice}

[class]{}-[func]{undoChoice}

[class]{}-[func]{backtrack}
```

=== „JS“

```javascript title="preorder_traversal_iii_template.js"
[class]{}-[func]{isSolution}

[class]{}-[func]{recordSolution}

[class]{}-[func]{isValid}

[class]{}-[func]{makeChoice}

[class]{}-[func]{undoChoice}

[class]{}-[func]{backtrack}
```

=== „TS“

```typescript title="preorder_traversal_iii_template.ts"
[class]{}-[func]{isSolution}

[class]{}-[func]{recordSolution}

[class]{}-[func]{isValid}

[class]{}-[func]{makeChoice}

[class]{}-[func]{undoChoice}

[class]{}-[func]{backtrack}
```

=== „Dart“

```dart title="preorder_traversal_iii_template.dart"
[class]{}-[func]{isSolution}

[class]{}-[func]{recordSolution}

[class]{}-[func]{isValid}

[class]{}-[func]{makeChoice}

[class]{}-[func]{undoChoice}

[class]{}-[func]{backtrack}
```

=== „Rost“

```rust title="preorder_traversal_iii_template.rs"
[class]{}-[func]{is_solution}

[class]{}-[func]{record_solution}

[class]{}-[func]{is_valid}

[class]{}-[func]{make_choice}

[class]{}-[func]{undo_choice}

[class]{}-[func]{backtrack}
```

=== „C“

```c title="preorder_traversal_iii_template.c"
[class]{}-[func]{isSolution}

[class]{}-[func]{recordSolution}

[class]{}-[func]{isValid}

[class]{}-[func]{makeChoice}

[class]{}-[func]{undoChoice}

[class]{}-[func]{backtrack}
```

=== „Zick“

```zig title="preorder_traversal_iii_template.zig"
[class]{}-[func]{isSolution}

[class]{}-[func]{recordSolution}

[class]{}-[func]{isValid}

[class]{}-[func]{makeChoice}

[class]{}-[func]{undoChoice}

[class]{}-[func]{backtrack}
```

Entsprechend der Bedeutung der Frage haben wir den Wert 7 7 gefundenDie Suche sollte nach dem Knoten 7 fortgesetzt werden, daher müssen die Anweisungen nach der Datensatzlösungreturngelöscht. Die folgende Abbildung vergleichtreturnden Suchvorgang für das Beibehalten oder Löschen von Anweisungen.

Fügen Sie hier eine Bildbeschreibung ein

Im Vergleich zur Code-Implementierung, die auf Preorder-Traversal basiert, erscheint die Code-Implementierung, die auf dem Backtracking-Algorithmus-Framework basiert, zwar ausführlicher, ist aber vielseitiger. Tatsächlich können viele Backtracking-Probleme in diesem Rahmen gelöst werden . Wir müssen nur jede Methode entsprechend dem spezifischen Problem definieren stateund choicesim Framework implementieren.

Allgemeine Begriffe

Um algorithmische Fragestellungen klarer zu analysieren, fassen wir die Bedeutung häufig verwendeter Begriffe in Backtracking-Algorithmen zusammen und geben entsprechende Beispiele im Vergleich zu Beispiel 3.

Tabelle Allgemeine Begriffe für Backtracking-Algorithmen

Substantiv Definition Beispiel drei
Lösung Eine Lösung ist eine Antwort, die bestimmte Bedingungen des Problems erfüllt, bei denen es sich um eine oder mehrere handeln kann Wurzelknoten zu Knoten 7 77. Alle Pfade, die die Einschränkungen erfüllen
Zwang Einschränkungen sind Bedingungen in einem Problem, die die Durchführbarkeit einer Lösung einschränken und werden normalerweise zum Beschneiden verwendet Knoten 3 3 ist nicht im Pfad enthalten3
StateState Der Status stellt die Situation des Problems zu einem bestimmten Zeitpunkt dar, einschließlich der getroffenen Entscheidungen Der aktuell besuchte Knotenpfad, also pathdie Knotenliste
TryAttempt Beim Ausprobieren handelt es sich um den Prozess des Erkundens des Lösungsraums auf der Grundlage verfügbarer Optionen, einschließlich des Treffens einer Auswahl, des Aktualisierens des Status und der Prüfung, ob es sich um eine Lösung handelt Greifen Sie rekursiv auf den linken (rechten) untergeordneten Knoten zu, fügen Sie den Knoten hinzu pathund bestimmen Sie, ob der Wert des Knotens 7 7 ist7
Zurückverfolgen Rollback bedeutet, die vorherige Auswahl aufzuheben und zum vorherigen Status zurückzukehren, wenn ein Status auftritt, der die Einschränkungen nicht erfüllt. Beim Überqueren des Blattknotens und Beenden des Knotenzugriffs beträgt der gefundene Wert 3 3Die Suche wird beendet, wenn Knoten 3 erreicht ist, und die Funktion kehrt zurück
Beschneidung Pruning ist eine Methode zur Vermeidung bedeutungsloser Suchpfade basierend auf Problemmerkmalen und Einschränkungen, wodurch die Sucheffizienz verbessert werden kann. Bei einem Wert von 3 3Wenn der Knoten 3 erreicht , wird die Suche beendet.

!!! Tipp

问题、解、状态等概念是通用的,在分治、回溯、动态规划、贪心等算法中都有涉及。

Vorteile und Einschränkungen

Der Backtracking-Algorithmus ist im Wesentlichen ein Tiefensuchalgorithmus, der alle möglichen Lösungen ausprobiert, bis er eine findet, die die Bedingungen erfüllt. Der Vorteil dieser Methode besteht darin, dass sie alle möglichen Lösungen finden kann und bei angemessenen Beschneidungsvorgängen sehr effizient ist.

Bei großen oder komplexen Problemen kann die Effizienz des Backtracking-Algorithmus jedoch inakzeptabel sein .

  • Zeit : Backtracking-Algorithmen müssen normalerweise alle Möglichkeiten des Zustandsraums durchlaufen, und die zeitliche Komplexität kann exponentielle oder faktorielle Ordnung erreichen.
  • Platz : Der aktuelle Status (z. B. Pfade, Hilfsvariablen zum Beschneiden usw.) muss in rekursiven Aufrufen gespeichert werden. Wenn die Tiefe groß ist, kann der Platzbedarf sehr groß werden.

Dennoch sind Backtracking-Algorithmen immer noch die beste Lösung für einige Suchprobleme und Probleme mit der Erfüllung von Einschränkungen . Da es bei diesen Problemen unmöglich ist, vorherzusagen, welche Entscheidungen zu einer gültigen Lösung führen, müssen wir alle möglichen Entscheidungen durchlaufen. In diesem Fall liegt der Schlüssel darin, die Effizienz zu optimieren . Es gibt zwei gängige Methoden zur Effizienzoptimierung.

  • Beschneiden : Sparen Sie Zeit und Platz, indem Sie die Suche nach Wegen vermeiden, die definitiv keine Lösung bringen.
  • Heuristische Suche : Einführung einiger Strategien oder Schätzungen während des Suchprozesses, um die Suche nach Pfaden zu priorisieren, die am wahrscheinlichsten zu effektiven Lösungen führen.

Sehen Sie sich typische Beispiele an

Backtracking-Algorithmen können zur Lösung zahlreicher Suchprobleme, Einschränkungenerfüllungsprobleme und kombinatorischer Optimierungsprobleme verwendet werden.

Suchprobleme : Das Ziel dieser Art von Problem besteht darin, eine Lösung zu finden, die bestimmte Bedingungen erfüllt.

  • Gesamtpermutationsproblem: Finden Sie bei einer gegebenen Menge alle möglichen Permutationen und Kombinationen.
  • Teilmengensummenproblem: Finden Sie bei einer gegebenen Menge und einer Zielsumme die Teilmenge, deren Summe in der Menge die Zielsumme ist.
  • Problem des Turms von Hanoi: Bei drei Säulen und einer Reihe von Scheiben unterschiedlicher Größe müssen alle Scheiben von einer Säule zur anderen bewegt werden. Es kann jeweils nur eine Scheibe bewegt werden, und die große Scheibe kann nicht auf einer platziert werden kleine Scheibe.

Probleme mit der Erfüllung von Einschränkungen : Das Ziel dieses Problemtyps besteht darin, eine Lösung zu finden, die alle Einschränkungen erfüllt.

  • nnn Königin: inn × nn \times nN×Platzierenn auf dem Schachbrett von nn Königinnen, damit sie sich nicht gegenseitig angreifen.
  • Sudoku: bei 9 × 9 9 \times 99×Füllen Sie das 9er -Raster mit den Zahlen 1 11 ~9 99 , so dass jede Zeile, jede Spalte und jede3 × 3 3 \times 33×Zahlen in 3 Untergittern werden nicht wiederholt.
  • Problem mit der Graphenfärbung: Färben Sie bei einem gegebenen ungerichteten Graphen jeden Scheitelpunkt des Graphen mit der geringsten Farbe, sodass benachbarte Scheitelpunkte unterschiedliche Farben haben.

Kombinatorische Optimierungsprobleme : Das Ziel dieser Art von Problem besteht darin, die optimale Lösung zu finden, die bestimmte Bedingungen in einem kombinatorischen Raum erfüllt.

  • 0-1 Rucksackproblem: Bei einer Reihe von Gegenständen und einem Rucksack hat jeder Gegenstand einen bestimmten Wert und ein bestimmtes Gewicht. Es ist erforderlich, Gegenstände auszuwählen, um den Gesamtwert innerhalb der Kapazitätsgrenze des Rucksacks zu maximieren.
  • Problem des Handlungsreisenden: Beginnen Sie in einer Grafik an einem Punkt, besuchen Sie alle anderen Punkte genau einmal und kehren Sie zum Ausgangspunkt zurück, um den kürzesten Weg zu finden.
  • Maximum-Clique-Problem: Finden Sie bei einem gegebenen ungerichteten Graphen den größten vollständigen Untergraphen, d. h. zwei beliebige Scheitelpunkte im Untergraphen sind durch eine Kante verbunden.

Beachten Sie, dass Backtracking für viele kombinatorische Optimierungsprobleme keine optimale Lösung ist.

  • Das 0-1-Rucksackproblem wird normalerweise durch dynamische Programmierung gelöst, um eine höhere Zeiteffizienz zu erreichen.
  • Der Handlungsreisende ist ein bekanntes NP-schweres Problem. Zu den häufig verwendeten Lösungen gehören der genetische Algorithmus und der Ameisenkolonie-Algorithmus.
  • Das Maximum-Clique-Problem ist ein klassisches Problem der Graphentheorie und kann durch heuristische Algorithmen wie Greedy gelöst werden.

Supongo que te gusta

Origin blog.csdn.net/zy_dreamer/article/details/132865393
Recomendado
Clasificación