Kürzeste universelle Labyrinth-Ausgangsschnur

48

Ein Labyrinth in einem N x N-Raster aus quadratischen Zellen wird definiert, indem angegeben wird, ob jede Kante eine Wand ist oder nicht. Alle Außenkanten sind Wände. Eine Zelle ist als Start definiert , und eine Zelle ist als Ausgang definiert , und der Ausgang ist von Anfang an erreichbar. Der Start und der Ausgang sind nie die gleiche Zelle.

Beachten Sie, dass sich weder Start noch Ausgang am äußeren Rand des Labyrinths befinden müssen. Dies ist also ein gültiges Labyrinth:

Ein 3 mal 3 Labyrinth mit dem Ausgang in der zentralen Zelle

Eine Reihe von 'N', 'E', 'S' und 'W' zeigt an, dass versucht wird, sich nach Norden, Osten, Süden und Westen zu bewegen. Ein Zug, der von einer Mauer blockiert wird, wird ohne Bewegung übersprungen. Eine Zeichenfolge verlässt ein Labyrinth, wenn das Anwenden dieser Zeichenfolge von Anfang an dazu führt, dass der Ausgang erreicht wird (unabhängig davon, ob die Zeichenfolge nach Erreichen des Ausgangs fortgesetzt wird).

Inspiriert von diesem Rätsel. SEINE Frage, für die xnor eine nachweisbare Methode zum Lösen mit einer sehr langen Zeichenfolge bereitstellte , schreibt Code, der eine einzelne Zeichenfolge findet, die aus einem beliebigen 3 x 3-Labyrinth herauskommt.

Mit Ausnahme ungültiger Labyrinthe (Start und Ende in derselben Zelle oder Ausgang von Anfang an nicht erreichbar) gibt es 138.172 gültige Labyrinthe, und die Zeichenfolge muss jeden von ihnen beenden.

Gültigkeit

Die Zeichenfolge muss Folgendes erfüllen:

  • Es besteht nur aus den Zeichen 'N', 'E', 'S' und 'W'.
  • Es verlässt jedes Labyrinth, auf das es angewendet wird, wenn es beim Start gestartet wird.

Da die Menge aller möglichen Labyrinthe jedes mögliche Labyrinth mit jedem möglichen gültigen Startpunkt enthält, bedeutet dies automatisch, dass die Zeichenfolge jedes Labyrinth von jedem gültigen Startpunkt verlässt . Das heißt, von jedem Ausgangspunkt aus, von dem aus der Ausgang erreichbar ist.

Gewinnen

Der Gewinner ist die Antwort, die die kürzeste gültige Zeichenfolge bereitstellt und den Code enthält, mit dem sie erstellt wurde. Wenn mehr als eine der Antworten eine Zeichenfolge mit dieser kürzesten Länge bereitstellt, gewinnt die erste, die diese Zeichenfolge veröffentlicht.

Beispiel

Hier ist eine 500 Zeichen lange Beispielzeichenfolge, um Ihnen etwas zu bieten, das Sie schlagen können:

SEENSSNESSWNNSNNNNWWNWENENNWEENSESSNENSESWENWWWWWENWNWWSESNSWENNWNWENWSSSNNNNNNESWNEWWWWWNNNSWESSEEWNENWENEENNEEESEENSSEENNWWWNWSWNSSENNNWESSESNWESWEENNWSNWWEEWWESNWEEEWWSSSESEEWWNSSEEEEESSENWWNNSWNENSESSNEESENEWSSNWNSEWEEEWEESWSNNNEWNNWNWSSWEESSSSNESESNENNWEESNWEWSWNSNWNNWENSNSWEWSWWNNWNSENESSNENEWNSSWNNEWSESWENEEENSWWSNNNNSSNENEWSNEEWNWENEEWEESEWEEWSSESSSWNWNNSWNWENWNENWNSWESNWSNSSENENNNWSSENSSSWWNENWWWEWSEWSNSSWNNSEWEWENSWENWSENEENSWEWSEWWSESSWWWNWSSEWSNWSNNWESNSNENNSNEWSNNESNNENWNWNNNEWWEWEE

Vielen Dank an orlp für die Spende.


Bestenliste

Bestenliste

Gleiche Punktzahlen werden in der Reihenfolge der Veröffentlichung dieser Punktzahl aufgelistet. Dies ist nicht unbedingt die Reihenfolge, in der die Antworten veröffentlicht wurden, da die Punktzahl für eine bestimmte Antwort im Laufe der Zeit aktualisiert werden kann.


Richter

Hier ist ein Python 3-Validator , der eine Zeichenfolge von NESW als Befehlszeilenargument oder über STDIN verwendet.

Bei einer ungültigen Zeichenfolge erhalten Sie ein visuelles Beispiel für ein Labyrinth, für das ein Fehler aufgetreten ist.

Trichoplax
quelle
3
Das ist eine wirklich nette Frage. Gibt es eine kürzeste Zeichenfolge (oder mehrere Zeichenfolgen und einen Beweis, dass es keine kürzeren Antworten geben kann)? Und wenn ja, weißt du es?
Alex Van Liew
1
@AlexReinking ja, der Start kann eine der 9 Zellen sein und der Ausgang kann eine der 9 Zellen sein, solange sie nicht dieselbe Zelle sind und der Ausgang von Anfang an erreichbar ist.
Trichoplax
1
Etwas ähnlich wie diese Frage Stackoverflow: stackoverflow.com/questions/26910401/... - aber beginnen und enden Zelle sind oben links und unten rechts in diesem einen, der die möglichen Labyrinth Zählung 2423. reduziert
schnaader
1
@ proudhaskeller so oder so wäre eine gültige Frage. Der für n = 3 bewertete allgemeine Fall würde einen allgemeineren Code erfordern. In diesem speziellen Fall sind Optimierungen möglich, die nicht für allgemeines n gelten, und so habe ich es gewählt, um es zu fragen.
Trichoplax
2
Hat jemand in Betracht gezogen, dieses Problem als die kürzeste akzeptierte Zeichenfolge für einen regulären Ausdruck zu betrachten? Es würde VIELE Reduzierungen der Anzahl der Probleme erfordern, bevor auf reguläre Ausdrücke umgestellt werden kann, könnte jedoch theoretisch eine nachweislich optimale Lösung finden.
Kyle McCormick

Antworten:

37

C ++, 97 95 93 91 86 83 82 81 79 Zeichen

NNWSWNNSENESESWSSWNSEENWNWSSEWNENWEENWSWNWSSENENWNESENESWNWSESEWWNENWNEES

Meine Strategie ist ziemlich einfach - ein Evolutionsalgorithmus, der gültige Sequenzen vergrößern, verkleinern, Elemente austauschen und mutieren kann. Meine Evolutionslogik ist jetzt fast dieselbe wie die von @ Sp3000, da er eine Verbesserung gegenüber meiner war.

Meine Implementierung der Labyrinthlogik ist jedoch ziemlich geschickt. Auf diese Weise kann ich mit rasender Geschwindigkeit prüfen, ob die Zeichenfolgen gültig sind. Versuchen Sie es anhand des Kommentars do_moveund des MazeKonstruktors herauszufinden .

#include <algorithm>
#include <bitset>
#include <cstdint>
#include <iostream>
#include <random>
#include <set>
#include <vector>

/*
    Positions:

        8, 10, 12
        16, 18, 20
        24, 26, 28

    By defining as enum respectively N, W, E, S as 0, 1, 2, 3 we get:

        N: -8, E: 2, S: 8, W: -2
        0: -8, 1: -2, 2: 2, 3: 8

    To get the indices for the walls, average the numbers of the positions it
    would be blocking. This gives the following indices:

        9, 11, 12, 14, 16, 17, 19, 20, 22, 24, 25, 27

    We'll construct a wall mask with a 1 bit for every position that does not
    have a wall. Then if a 1 shifted by the average of the positions AND'd with
    the wall mask is zero, we have hit a wall.
*/

enum { N = -8, W = -2, E = 2, S = 8 };
static const int encoded_pos[] = {8, 10, 12, 16, 18, 20, 24, 26, 28};
static const int wall_idx[] = {9, 11, 12, 14, 16, 17, 19, 20, 22, 24, 25, 27};
static const int move_offsets[] = { N, W, E, S };

int do_move(uint32_t walls, int pos, int move) {
    int idx = pos + move / 2;
    return walls & (1ull << idx) ? pos + move : pos;
}

struct Maze {
    uint32_t walls;
    int start, end;

    Maze(uint32_t maze_id, int start, int end) {
        walls = 0;
        for (int i = 0; i < 12; ++i) {
            if (maze_id & (1 << i)) walls |= 1 << wall_idx[i];
        }
        this->start = encoded_pos[start];
        this->end = encoded_pos[end];
    }

    uint32_t reachable() {
        if (start == end) return false;

        uint32_t reached = 0;
        std::vector<int> fill; fill.reserve(8); fill.push_back(start);
        while (fill.size()) {
            int pos = fill.back(); fill.pop_back();
            if (reached & (1 << pos)) continue;
            reached |= 1 << pos;
            for (int m : move_offsets) fill.push_back(do_move(walls, pos, m));
        }

        return reached;
    }

    bool interesting() {
        uint32_t reached = reachable();
        if (!(reached & (1 << end))) return false;
        if (std::bitset<32>(reached).count() <= 4) return false;

        int max_deg = 0;
        uint32_t ends = 0;
        for (int p = 0; p < 9; ++p) {
            int pos = encoded_pos[p];
            if (reached & (1 << pos)) {
                int deg = 0;
                for (int m : move_offsets) {
                    if (pos != do_move(walls, pos, m)) ++deg;
                }
                if (deg == 1) ends |= 1 << pos;
                max_deg = std::max(deg, max_deg);
            }
        }

        if (max_deg <= 2 && ends != ((1u << start) | (1u << end))) return false;

        return true;
    }
};

std::vector<Maze> gen_valid_mazes() {
    std::vector<Maze> mazes;
    for (int maze_id = 0; maze_id < (1 << 12); maze_id++) {
        for (int points = 0; points < 9*9; ++points) {
            Maze maze(maze_id, points % 9, points / 9);
            if (!maze.interesting()) continue;
            mazes.push_back(maze);
        }
    }

    return mazes;
}

bool is_solution(const std::vector<int>& moves, Maze maze) {
    int pos = maze.start;
    for (auto move : moves) {
        pos = do_move(maze.walls, pos, move);
        if (pos == maze.end) return true;
    }

    return false;
}

std::vector<int> str_to_moves(std::string str) {
    std::vector<int> moves;
    for (auto c : str) {
        switch (c) {
        case 'N': moves.push_back(N); break;
        case 'E': moves.push_back(E); break;
        case 'S': moves.push_back(S); break;
        case 'W': moves.push_back(W); break;
        }
    }

    return moves;
}

std::string moves_to_str(const std::vector<int>& moves) {
    std::string result;
    for (auto move : moves) {
             if (move == N) result += "N";
        else if (move == E) result += "E";
        else if (move == S) result += "S";
        else if (move == W) result += "W";
    }
    return result;
}

bool solves_all(const std::vector<int>& moves, std::vector<Maze>& mazes) {
    for (size_t i = 0; i < mazes.size(); ++i) {
        if (!is_solution(moves, mazes[i])) {
            // Bring failing maze closer to begin.
            std::swap(mazes[i], mazes[i / 2]);
            return false;
        }
    }
    return true;
}

template<class Gen>
int randint(int lo, int hi, Gen& gen) {
    return std::uniform_int_distribution<int>(lo, hi)(gen);
}

template<class Gen>
int randmove(Gen& gen) { return move_offsets[randint(0, 3, gen)]; }

constexpr double mutation_p = 0.35; // Chance to mutate.
constexpr double grow_p = 0.1; // Chance to grow.
constexpr double swap_p = 0.2; // Chance to swap.

int main(int argc, char** argv) {
    std::random_device rnd;
    std::mt19937 rng(rnd());
    std::uniform_real_distribution<double> real;
    std::exponential_distribution<double> exp_big(0.5);
    std::exponential_distribution<double> exp_small(2);

    std::vector<Maze> mazes = gen_valid_mazes();

    std::vector<int> moves;
    while (!solves_all(moves, mazes)) {
        moves.clear();
        for (int m = 0; m < 500; m++) moves.push_back(randmove(rng));
    }

    size_t best_seen = moves.size();
    std::set<std::vector<int>> printed;
    while (true) {
        std::vector<int> new_moves(moves);
        double p = real(rng);

        if (p < grow_p && moves.size() < best_seen + 10) {
            int idx = randint(0, new_moves.size() - 1, rng);
            new_moves.insert(new_moves.begin() + idx, randmove(rng));
        } else if (p < swap_p) {
            int num_swap = std::min<int>(1 + exp_big(rng), new_moves.size()/2);
            for (int i = 0; i < num_swap; ++i) {
                int a = randint(0, new_moves.size() - 1, rng);
                int b = randint(0, new_moves.size() - 1, rng);
                std::swap(new_moves[a], new_moves[b]);
            }
        } else if (p < mutation_p) {
            int num_mut = std::min<int>(1 + exp_big(rng), new_moves.size());
            for (int i = 0; i < num_mut; ++i) {
                int idx = randint(0, new_moves.size() - 1, rng);
                new_moves[idx] = randmove(rng);
            }
        } else {
            int num_shrink = std::min<int>(1 + exp_small(rng), new_moves.size());
            for (int i = 0; i < num_shrink; ++i) {
                int idx = randint(0, new_moves.size() - 1, rng);
                new_moves.erase(new_moves.begin() + idx);
            }
        }

        if (solves_all(new_moves, mazes)) {
            moves = new_moves;

            if (moves.size() <= best_seen && !printed.count(moves)) {
                std::cout << moves.size() << " " << moves_to_str(moves) << "\n";
                if (moves.size() < best_seen) {
                    printed.clear(); best_seen = moves.size();
                }
                printed.insert(moves);
            }
        }
    }

    return 0;
}
orlp
quelle
5
Bestätigt gültig. Ich bin beeindruckt - ich hatte nicht erwartet, dass die Saiten so kurz sind.
Trichoplax
2
Endlich bin ich dazu gekommen, gcc zu installieren und das für mich selbst zu machen. Es ist hypnotisch zu beobachten, wie die Saiten mutieren und langsam schrumpfen ...
Trichoplax
1
@ Trichoplax Ich habe dir gesagt, es hat Spaß gemacht :)
Orlp
2
@AlexReinking Ich habe meine Antwort mit dieser Implementierung aktualisiert. Wenn Sie sich die Demontage ansehen, werden Sie feststellen, dass es sich nur um ein Dutzend Anweisungen ohne Verzweigung oder Last handelt: coliru.stacked-crooked.com/a/3b09d36db85ce793 .
Orlp
2
@AlexReinking Fertig. do_moveist jetzt wahnsinnig schnell.
Orlp
16

Python 3 + PyPy, 82 80 Zeichen

SWWNNSENESESWSSWSEENWNWSWSEWNWNENENWWSESSEWSWNWSENWEENWWNNESENESSWNWSESESWWNNESE

Ich habe gezögert, diese Antwort zu posten, weil ich im Grunde genommen den Ansatz von orlp gewählt und meine eigene Richtung eingeschlagen habe. Diese Saite wurde ausgehend von einer Lösung mit einer Pseudozufallslänge von 500 gefunden - es wurden eine ganze Reihe von Samen ausprobiert, bevor ich den aktuellen Rekord brechen konnte.

Die einzige große Neuerung ist, dass ich nur ein Drittel der Labyrinthe ansehe. Zwei Kategorien von Labyrinthen sind von der Suche ausgeschlossen:

  • Labyrinthe, bei denen <= 7Plätze erreichbar sind
  • Labyrinthe, bei denen sich alle erreichbaren Felder auf einem einzigen Pfad befinden und Start / Ziel nicht an beiden Enden sind

Die Idee ist, dass jede Zeichenfolge, die den Rest der Irrgärten löst, auch das oben Genannte automatisch lösen sollte. Ich bin davon überzeugt, dass dies für den zweiten Typ zutrifft, für den ersten jedoch definitiv nicht. Daher enthält die Ausgabe einige False Positives, die separat überprüft werden müssen. Diese falschen Positiven lassen normalerweise nur etwa 20 Irrgärten aus, daher dachte ich, es wäre ein guter Kompromiss zwischen Geschwindigkeit und Genauigkeit, und es würde den Saiten auch ein wenig mehr Luft zum Mutieren geben.

Anfangs habe ich eine lange Liste von Suchheuristiken durchgesehen, aber schrecklicherweise hat keine von ihnen etwas Besseres als 140 oder so gefunden.

import random

N, M = 3, 3

W = 2*N-1
H = 2*M-1

random.seed(142857)


def move(c, cell, walls):
    global W, H

    if c == "N":
        if cell > W and not (1<<(cell-W)//2 & walls):
            cell = cell - W*2

    elif c == "S":
        if cell < W*(H-1) and not (1<<(cell+W)//2 & walls):
            cell = cell + W*2

    elif c == "E":
        if cell % W < W-1 and not (1<<(cell+1)//2 & walls):
            cell = cell + 2

    elif c == "W":
        if cell % W > 0 and not (1<<(cell-1)//2 & walls):
            cell = cell - 2

    return cell


def valid_maze(start, finish, walls):
    global adjacent

    if start == finish:
        return False

    visited = set()
    cells = [start]

    while cells:
        curr_cell = cells.pop()

        if curr_cell == finish:
            return True

        if curr_cell in visited:
            continue

        visited.add(curr_cell)

        for c in "NSEW":
            cells.append(move(c, curr_cell, walls))

    return False


def print_maze(maze):
    start, finish, walls = maze
    print_str = "".join(" #"[walls & (1 << i//2) != 0] if i%2 == 1
                        else " SF"[2*(i==finish) + (i==start)]
                        for i in range(W*H))

    print("#"*(H+2))

    for i in range(H):
        print("#" + print_str[i*W:(i+1)*W] + "#")

    print("#"*(H+2), end="\n\n")

all_cells = [W*y+x for y in range(0, H, 2) for x in range(0, W, 2)]
mazes = []

for start in all_cells:
    for finish in all_cells:
        for walls in range(1<<(N*(M-1) + M*(N-1))):
            if valid_maze(start, finish, walls):
                mazes.append((start, finish, walls))

num_mazes = len(mazes)
print(num_mazes, "mazes generated")

to_remove = set()

for i, maze in enumerate(mazes):
    start, finish, walls = maze

    reachable = set()
    cells = [start]

    while cells:
        cell = cells.pop()

        if cell in reachable:
            continue

        reachable.add(cell)

        if cell == finish:
            continue

        for c in "NSEW":
            new_cell = move(c, cell, walls)
            cells.append(new_cell)

    max_deg = 0
    sf = set()

    for cell in reachable:
        deg = 0

        for c in "NSEW":
            if move(c, cell, walls) != cell:
                deg += 1

        max_deg = max(deg, max_deg)

        if deg == 1:
            sf.add(cell)

    if max_deg <= 2 and len(sf) == 2 and sf != {start, finish}:
        # Single path subset
        to_remove.add(i)

    elif len(reachable) <= (N*M*4)//5:
        # Low reachability maze, above ratio is adjustable
        to_remove.add(i)

mazes = [maze for i,maze in enumerate(mazes) if i not in to_remove]
print(num_mazes - len(mazes), "mazes removed,", len(mazes), "remaining")
num_mazes = len(mazes)


def check(string, cache = set()):
    global mazes

    if string in cache:
        return True

    for i, maze in enumerate(mazes):
        start, finish, walls = maze
        cell = start

        for c in string:
            cell = move(c, cell, walls)

            if cell == finish:
                break

        else:
            # Swap maze to front
            mazes[i//2], mazes[i] = mazes[i], mazes[i//2]
            return False

    cache.add(string)
    return True


while True:
    string = "".join(random.choice("NSEW") for _ in range(500))

    if check(string):
        break

# string = "NWWSSESNESESNNWNNSWNWSSENESWSWNENENWNWESESENNESWSESWNWSWNNEWSESWSEEWNENWWSSNNEESS"

best = len(string)
seen = set()

while True:
    action = random.random()

    if action < 0.1:
        # Grow
        num_grow = int(random.expovariate(lambd=3)) + 1
        new_string = string

        for _ in range(num_grow):
            i = random.randrange(len(new_string))
            new_string = new_string[:i] + random.choice("NSEW") + new_string[i:]

    elif action < 0.2:
        # Swap
        num_swap = int(random.expovariate(lambd=1)) + 1
        new_string = string

        for _ in range(num_swap):
            i,j = sorted(random.sample(range(len(new_string)), 2))
            new_string = new_string[:i] + new_string[j] + new_string[i+1:j] + new_string[i] + new_string[j+1:]

    elif action < 0.35:
        # Mutate
        num_mutate = int(random.expovariate(lambd=1)) + 1
        new_string = string

        for _ in range(num_mutate):
            i = random.randrange(len(new_string))
            new_string = new_string[:i] + random.choice("NSEW") + new_string[i+1:]

    else:
        # Shrink
        num_shrink = int(random.expovariate(lambd=3)) + 1
        new_string = string

        for _ in range(num_shrink):
            i = random.randrange(len(new_string))
            new_string = new_string[:i] + new_string[i+1:]


    if check(new_string):
        string = new_string

    if len(string) <= best and string not in seen:
        while True:
            if len(string) < best:
                seen = set()

            seen.add(string)
            best = len(string)
            print(string, len(string))

            # Force removals on new record strings
            for i in range(len(string)):
                new_string = string[:i] + string[i+1:]

                if check(new_string):
                    string = new_string
                    break

            else:
                break
Sp3000
quelle
Bestätigt gültig. Schöne Verbesserungen :)
Trichoplax
Ich mag deine Vorstellung, dass einige Labyrinthe nicht überprüft werden müssen. Könnten Sie den Prozess der Ermittlung, welche Labyrinthe redundante Prüfungen sind, irgendwie automatisieren? Ich bin neugierig zu wissen, ob das mehr Labyrinthe aufzeigt als die, die intuitiv abgeleitet werden können ...
Trichoplax
Was ist Ihre Begründung dafür, dass Sie keine Pfadgraphen überprüfen müssen, bei denen der Start nicht an einem Ende liegt? Der Fall, in dem das Finish nicht an einem Ende liegt, ist einfach zu rechtfertigen und kann verstärkt werden, sodass Fälle, in denen das Finish ein Schnittscheitelpunkt ist, nicht überprüft werden müssen, aber ich kann nicht nachvollziehen, wie das Eliminieren von Startscheitelpunkten gerechtfertigt ist.
Peter Taylor
@PeterTaylor Nach einigem Nachdenken, theoretisch hast du recht, gibt es einige Labyrinthe, die du so nicht beseitigen kannst. Es scheint jedoch, dass es bei 3x3 für Saiten nicht so lange wichtig ist.
Orlp
2
@orlp, Sp3000 hat im Chat einen Beweis skizziert. Pfadgraphen sind ein Sonderfall. Nummerieren Sie die Zellen 0zu nauf dem Weg und nehmen wir an, dass die String SSie bekommt von 0zu n. Dann Skommst du auch von irgendeiner Zwischenzelle czu n. Angenommen, anders. Sei a(i)die Position nach iSchritten beginnend bei 0und b(i)beginnend bei c. Dann a(0) = 0 < b(0)ändert sich jeder Schritt aund bum höchstens 1 und a(|S|) = n > b(|S|). Nimm das Kleinste tso, dass a(t) >= b(t). Natürlich a(t) != b(t)oder sie wären synchron, also müssen sie schrittweise die Plätze tauschen, tindem sie sich in die gleiche Richtung bewegen.
Peter Taylor
3

C ++ und die Bibliothek von Lingeling

Zusammenfassung: Ein neuer Ansatz, keine neuen Lösungen , ein schönes Programm zum Spielen und einige interessante Ergebnisse der lokalen Nichtverbesserbarkeit der bekannten Lösungen. Oh, und einige allgemein nützliche Beobachtungen.

Mit einem SAT- basierten Ansatz konnte ich das ähnliche Problem für 4x4-Labyrinthe mit blockierten Zellen anstelle von dünnen Wänden und festen Start- und Ausgangspositionen an gegenüberliegenden Ecken vollständig lösen . Also hoffte ich, die gleichen Ideen für dieses Problem verwenden zu können. Obwohl ich für das andere Problem nur 2423 Labyrinthe verwendet habe (in der Zwischenzeit wurde beobachtet, dass 2083 genug sind) und eine Lösung der Länge 29 hat, hat die SAT-Codierung Millionen von Variablen verwendet und das Lösen hat Tage gedauert.

Deshalb habe ich beschlossen, den Ansatz auf zwei wichtige Arten zu ändern:

  • Bestehen Sie nicht darauf, eine Lösung von Grund auf zu suchen, sondern lassen Sie einen Teil der Lösungszeichenfolge reparieren. (Das ist durch Hinzufügen von Unit-Klauseln ohnehin einfach zu tun, aber mein Programm macht es bequem.)
  • Benutze nicht alle Labyrinthe von Anfang an. Fügen Sie stattdessen schrittweise jeweils ein ungelöstes Labyrinth hinzu. Einige Irrgärten können zufällig gelöst werden, oder sie werden immer dann gelöst, wenn die bereits in Betracht gezogenen gelöst sind. Im letzteren Fall wird es niemals hinzugefügt, ohne dass wir die Auswirkungen kennen müssen.

Ich habe auch einige Optimierungen vorgenommen, um weniger Variablen und Unit-Klauseln zu verwenden.

Das Programm basiert auf @ orlp's. Eine wichtige Änderung war die Auswahl der Labyrinthe:

  • Erstens sind Labyrinthe nur durch ihre Wandstruktur und die Startposition gegeben. (Sie speichern auch die erreichbaren Positionen.) Die Funktion is_solutionprüft, ob alle erreichbaren Positionen erreicht sind.
  • (Unverändert: Es werden immer noch keine Labyrinthe mit nur 4 oder weniger erreichbaren Positionen verwendet. Die meisten würden jedoch durch die folgenden Beobachtungen ohnehin weggeworfen.)
  • Wenn in einem Labyrinth keine der drei oberen Zellen verwendet wird, entspricht dies einem nach oben verschobenen Labyrinth. Also können wir es fallen lassen. Ebenso für ein Labyrinth, das keine der drei linken Zellen verwendet.
  • Es spielt keine Rolle, ob nicht erreichbare Teile verbunden sind, daher bestehen wir darauf, dass jede nicht erreichbare Zelle vollständig von Wänden umgeben ist.
  • Ein Labyrinth mit einem einzelnen Pfad, das ein Submaze eines größeren Labyrinths mit einem einzelnen Pfad ist, wird immer dann gelöst, wenn das größere Labyrinth gelöst wird. Wir brauchen es also nicht. Jedes einzelne Pfadlabyrinth mit einer Größe von höchstens 7 ist Teil eines größeren Labyrinths (das immer noch in 3x3 passt), aber es gibt einzelne Pfadlabyrinthe der Größe 8, die es nicht sind. Lassen Sie uns der Einfachheit halber nur Einzelpfad-Labyrinthe mit einer Größe von weniger als 8 fallen. (Und ich verwende immer noch, dass nur die Extrempunkte als Startpositionen betrachtet werden müssen. Alle Positionen werden als Ausgangspositionen verwendet, was nur für den SAT-Teil von Bedeutung ist des Programms.)

Auf diese Weise erhalte ich insgesamt 10772 Labyrinthe mit Startpositionen.

Hier ist das Programm:

#include <algorithm>
#include <array>
#include <bitset>
#include <cstring>
#include <iostream>
#include <set>
#include <vector>
#include <limits>
#include <cassert>

extern "C"{
#include "lglib.h"
}

// reusing a lot of @orlp's ideas and code

enum { N = -8, W = -2, E = 2, S = 8 };
static const int encoded_pos[] = {8, 10, 12, 16, 18, 20, 24, 26, 28};
static const int wall_idx[] = {9, 11, 12, 14, 16, 17, 19, 20, 22, 24, 25, 27};
static const int move_offsets[] = { N, E, S, W };
static const uint32_t toppos = 1ull << 8 | 1ull << 10 | 1ull << 12;
static const uint32_t leftpos = 1ull << 8 | 1ull << 16 | 1ull << 24;
static const int unencoded_pos[] = {0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,3,
                                    0,4,0,5,0,0,0,6,0,7,0,8};

int do_move(uint32_t walls, int pos, int move) {
  int idx = pos + move / 2;
  return walls & (1ull << idx) ? pos + move : pos;
}

struct Maze {
  uint32_t walls, reach;
  int start;

  Maze(uint32_t walls=0, uint32_t reach=0, int start=0):
    walls(walls),reach(reach),start(start) {}

  bool is_dummy() const {
    return (walls==0);
  }

  std::size_t size() const{
    return std::bitset<32>(reach).count();
  }

  std::size_t simplicity() const{  // how many potential walls aren't there?
    return std::bitset<32>(walls).count();
  }

};

bool cmp(const Maze& a, const Maze& b){
  auto asz = a.size();
  auto bsz = b.size();
  if (asz>bsz) return true;
  if (asz<bsz) return false;
  return a.simplicity()<b.simplicity();
}

uint32_t reachable(uint32_t walls) {
  static int fill[9];
  uint32_t reached = 0;
  uint32_t reached_relevant = 0;
  for (int start : encoded_pos){
    if ((1ull << start) & reached) continue;
    uint32_t reached_component = (1ull << start);
    fill[0]=start;
    int count=1;
    for(int i=0; i<count; ++i)
      for(int m : move_offsets) {
        int newpos = do_move(walls, fill[i], m);
        if (reached_component & (1ull << newpos)) continue;
        reached_component |= 1ull << newpos;
        fill[count++] = newpos;
      }
    if (count>1){
      if (reached_relevant)
        return 0;  // more than one nonsingular component
      if (!(reached_component & toppos) || !(reached_component & leftpos))
        return 0;  // equivalent to shifted version
      if (std::bitset<32>(reached_component).count() <= 4)
        return 0;  
      reached_relevant = reached_component;
    }
    reached |= reached_component;
  }
  return reached_relevant;
}

void enterMazes(uint32_t walls, uint32_t reached, std::vector<Maze>& mazes){
  int max_deg = 0;
  uint32_t ends = 0;
  for (int pos : encoded_pos)
    if (reached & (1ull << pos)) {
      int deg = 0;
      for (int m : move_offsets) {
        if (pos != do_move(walls, pos, m))
          ++deg;
      }
      if (deg == 1)
        ends |= 1ull << pos;
      max_deg = std::max(deg, max_deg);
    }
  uint32_t starts = reached;
  if (max_deg == 2){
    if (std::bitset<32>(reached).count() <= 7)
      return; // small paths are redundant
    starts = ends; // need only start at extremal points
  }
  for (int pos : encoded_pos)
    if ( starts & (1ull << pos))
      mazes.emplace_back(walls, reached, pos);
}

std::vector<Maze> gen_valid_mazes() {
  std::vector<Maze> mazes;
  for (int maze_id = 0; maze_id < (1 << 12); maze_id++) {
    uint32_t walls = 0;
    for (int i = 0; i < 12; ++i) 
      if (maze_id & (1 << i))
    walls |= 1ull << wall_idx[i];
    uint32_t reached=reachable(walls);
    if (!reached) continue;
    enterMazes(walls, reached, mazes);
  }
  std::sort(mazes.begin(),mazes.end(),cmp);
  return mazes;
};

bool is_solution(const std::vector<int>& moves, Maze& maze) {
  int pos = maze.start;
  uint32_t reached = 1ull << pos;
  for (auto move : moves) {
    pos = do_move(maze.walls, pos, move);
    reached |= 1ull << pos;
    if (reached == maze.reach) return true;
  }
  return false;
}

std::vector<int> str_to_moves(std::string str) {
  std::vector<int> moves;
  for (auto c : str) {
    switch (c) {
    case 'N': moves.push_back(N); break;
    case 'E': moves.push_back(E); break;
    case 'S': moves.push_back(S); break;
    case 'W': moves.push_back(W); break;
    }
  }
  return moves;
}

Maze unsolved(const std::vector<int>& moves, std::vector<Maze>& mazes) {
  int unsolved_count = 0;
  Maze problem{};
  for (Maze m : mazes)
    if (!is_solution(moves, m))
      if(!(unsolved_count++))
    problem=m;
  if (unsolved_count)
    std::cout << "unsolved: " << unsolved_count << "\n";
  return problem;
}

LGL * lgl;

constexpr int TRUELIT = std::numeric_limits<int>::max();
constexpr int FALSELIT = -TRUELIT;

int new_var(){
  static int next_var = 1;
  assert(next_var<TRUELIT);
  return next_var++;
}

bool lit_is_true(int lit){
  int abslit = lit>0 ? lit : -lit;
  bool res = (abslit==TRUELIT) || (lglderef(lgl,abslit)>0);
  return lit>0 ? res : !res;
}

void unsat(){
  std::cout << "Unsatisfiable!\n";
  std::exit(1);
}

void clause(const std::set<int>& lits){
  if (lits.find(TRUELIT) != lits.end())
    return;
  for (int lit : lits)
    if (lits.find(-lit) != lits.end())
      return;
  int found=0;
  for (int lit : lits)
    if (lit != FALSELIT){
      lgladd(lgl, lit);
      found=1;
    }
  lgladd(lgl, 0);
  if (!found)
    unsat();
}

void at_most_one(const std::set<int>& lits){
  if (lits.size()<2)
    return;
  for(auto it1=lits.cbegin(); it1!=lits.cend(); ++it1){
    auto it2=it1;
    ++it2;
    for(  ; it2!=lits.cend(); ++it2)
      clause( {- *it1, - *it2} );
  }
}

/* Usually, lit_op(lits,sgn) creates a new variable which it returns,
   and adds clauses that ensure that the variable is equivalent to the
   disjunction (if sgn==1) or the conjunction (if sgn==-1) of the literals
   in lits. However, if this disjunction or conjunction is constant True
   or False or simplifies to a single literal, that is returned without
   creating a new variable and without adding clauses.                    */ 

int lit_op(std::set<int> lits, int sgn){
  if (lits.find(sgn*TRUELIT) != lits.end())
    return sgn*TRUELIT;
  lits.erase(sgn*FALSELIT);
  if (!lits.size())
    return sgn*FALSELIT;
  if (lits.size()==1)
    return *lits.begin();
  int res=new_var();
  for(int lit : lits)
    clause({sgn*res,-sgn*lit});
  for(int lit : lits)
    lgladd(lgl,sgn*lit);
  lgladd(lgl,-sgn*res);
  lgladd(lgl,0);
  return res;
}

int lit_or(std::set<int> lits){
  return lit_op(lits,1);
}

int lit_and(std::set<int> lits){
  return lit_op(lits,-1);
}

using A4 = std::array<int,4>;

void add_maze_conditions(Maze m, std::vector<A4> dirs, int len){
  int mp[9][2];
  int rp[9];
  for(int p=0; p<9; ++p)
    if((1ull << encoded_pos[p]) & m.reach)
      rp[p] = mp[p][0] = encoded_pos[p]==m.start ? TRUELIT : FALSELIT;
  int t=0;
  for(int i=0; i<len; ++i){
    std::set<int> posn {};
    for(int p=0; p<9; ++p){
      int ep = encoded_pos[p];
      if((1ull << ep) & m.reach){
        std::set<int> reach_pos {};
        for(int d=0; d<4; ++d){
          int np = do_move(m.walls, ep, move_offsets[d]);
          reach_pos.insert( lit_and({mp[unencoded_pos[np]][t],
                                  dirs[i][d ^ ((np==ep)?0:2)]    }));
        }
        int pl = lit_or(reach_pos);
        mp[p][!t] = pl;
        rp[p] = lit_or({rp[p], pl});
        posn.insert(pl);
      }
    }
    at_most_one(posn);
    t=!t;
  }
  for(int p=0; p<9; ++p)
    if((1ull << encoded_pos[p]) & m.reach)
      clause({rp[p]});
}

void usage(char* argv0){
  std::cout << "usage: " << argv0 <<
    " <string>\n   where <string> consists of 'N', 'E', 'S', 'W' and '*'.\n" ;
  std::exit(2);
}

const std::string nesw{"NESW"};

int main(int argc, char** argv) {
  if (argc!=2)
    usage(argv[0]);
  std::vector<Maze> mazes = gen_valid_mazes();
  std::cout << "Mazes with start positions: " << mazes.size() << "\n" ;
  lgl = lglinit();
  int len = std::strlen(argv[1]);
  std::cout << argv[1] << "\n   with length " << len << "\n";

  std::vector<A4> dirs;
  for(int i=0; i<len; ++i){
    switch(argv[1][i]){
    case 'N':
      dirs.emplace_back(A4{TRUELIT,FALSELIT,FALSELIT,FALSELIT});
      break;
    case 'E':
      dirs.emplace_back(A4{FALSELIT,TRUELIT,FALSELIT,FALSELIT});
      break;
    case 'S':
      dirs.emplace_back(A4{FALSELIT,FALSELIT,TRUELIT,FALSELIT});
      break;
    case 'W':
      dirs.emplace_back(A4{FALSELIT,FALSELIT,FALSELIT,TRUELIT});
      break;
    case '*': {
      dirs.emplace_back();
      std::generate_n(dirs[i].begin(),4,new_var);
      std::set<int> dirs_here { dirs[i].begin(), dirs[i].end() };
      at_most_one(dirs_here);
      clause(dirs_here);
      for(int l : dirs_here)
        lglfreeze(lgl,l);
      break;
      }
    default:
      usage(argv[0]);
    }
  }

  int maze_nr=0;
  for(;;) {
    std::cout << "Solving...\n";
    int res=lglsat(lgl);
    if(res==LGL_UNSATISFIABLE)
      unsat();
    assert(res==LGL_SATISFIABLE);
    std::string sol(len,' ');
    for(int i=0; i<len; ++i)
      for(int d=0; d<4; ++d)
        if (lit_is_true(dirs[i][d])){
          sol[i]=nesw[d];
          break;
    }
    std::cout << sol << "\n";

    Maze m=unsolved(str_to_moves(sol),mazes);
    if (m.is_dummy()){
      std::cout << "That solves all!\n";
      return 0;
    }
    std::cout << "Adding maze " << ++maze_nr << ": " << 
      m.walls << "/" << m.start <<
      " (" << m.size() << "/" << 12-m.simplicity() << ")\n";
    add_maze_conditions(m,dirs,len);
  }
}  

Zuerst configure.shund makeder lingelingLöser, dann kompilieren Sie das Programm mit so etwas wie g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl, wo ...ist der Pfad, wo lglib.hbzw. liblgl.asind, so könnten zum Beispiel beide sein ../lingeling-<version>. Oder legen Sie sie einfach in dasselbe Verzeichnis und verzichten Sie auf die Optionen -Iund -L.

Das Programm nimmt ein obligatorisches Befehlszeilenargument, eine Zeichenfolge , bestehend aus N, E, S, W(für feste Richtungen) oder *. Sie können also nach einer allgemeinen Lösung der Größe 78 suchen, indem Sie eine Zeichenfolge von 78 *s (in Anführungszeichen) eingeben, oder nach einer Lösung suchen, die mit NEWSder Verwendung von NEWSgefolgt von beliebig vielen *s für zusätzliche Schritte beginnt . Nehmen Sie als ersten Test Ihre Lieblingslösung und ersetzen Sie einige Buchstaben durch *. Dies findet schnell eine Lösung für einen überraschend hohen Wert von "some".

Das Programm erkennt das hinzugefügte Labyrinth anhand der Wandstruktur und der Startposition und gibt die Anzahl der erreichbaren Positionen und Wände an. Die Labyrinthe werden nach diesen Kriterien sortiert und die erste ungelöste wird hinzugefügt. Daher haben die meisten Labyrinthe hinzugefügt (9/4), aber manchmal erscheinen auch andere.

Ich nahm die bekannte Lösung der Länge 79 und versuchte, sie für jede Gruppe von 26 benachbarten Buchstaben durch 25 beliebige Buchstaben zu ersetzen. Ich habe auch versucht, 13 Buchstaben am Anfang und am Ende zu entfernen und sie durch 13 am Anfang und 12 am Ende zu ersetzen und umgekehrt. Leider ist alles unbefriedigend ausgefallen. Können wir dies als Indikator dafür nehmen, dass die Länge 79 optimal ist? Nein, ich habe in ähnlicher Weise versucht, die Lösung für Länge 80 auf Länge 79 zu verbessern, und das war auch nicht erfolgreich.

Schließlich habe ich versucht, den Anfang einer Lösung mit dem Ende der anderen zu kombinieren und auch mit einer Lösung, die durch eine der Symmetrien transformiert wurde. Jetzt gehen mir die interessanten Ideen aus und ich habe beschlossen, Ihnen zu zeigen, was ich habe, obwohl es nicht zu neuen Lösungen geführt hat.

Christian Sievers
quelle
Das war eine wirklich interessante Lektüre. Sowohl der neue Ansatz als auch die verschiedenen Möglichkeiten, die Anzahl der zu überprüfenden Labyrinthe zu verringern. Um eine gültige Antwort zu erhalten, muss eine gültige Zeichenfolge angegeben werden. Es muss sich nicht um eine neue kürzeste Zeichenfolge handeln, sondern um eine gültige Zeichenfolge beliebiger Länge, um eine aktuelle Punktzahl für diesen Ansatz zu erhalten. Ich erwähne dies, weil die Antwort ohne Punktzahl in Gefahr ist, gelöscht zu werden, und ich möchte wirklich, dass sie erhalten bleibt.
Trichoplax
Auch eine gute Arbeit, um die optimale Längenlösung für die ältere Herausforderung zu finden !
Trichoplax