Najkrótszy uniwersalny ciąg wyjściowy labiryntu

48

Labirynt na siatce N przez N kwadratowych komórek jest definiowany przez określenie, czy każda krawędź jest ścianą, czy nie. Wszystkie zewnętrzne krawędzie są ścianami. Jedna komórka jest zdefiniowana jako początek , a jedna komórka jest zdefiniowana jako wyjście , a wyjście jest osiągalne od początku. Początek i koniec nigdy nie są tą samą komórką.

Pamiętaj, że ani początek, ani wyjście nie musi znajdować się na zewnętrznej granicy labiryntu, więc jest to prawidłowy labirynt:

Labirynt 3 na 3 z wyjściem na centralnej celi

Ciąg „N”, „E”, „S” i „W” oznacza próbę przejścia odpowiednio na północ, wschód, południe i zachód. Ruch blokowany przez ścianę jest pomijany bez ruchu. Łańcuch wychodzi z labiryntu, jeśli zastosowanie go od początku powoduje, że wyjście jest osiągane (niezależnie od tego, czy ciąg kontynuuje po osiągnięciu wyjścia).

Zainspirowany to pytanie puzzling.SE dla których XNOR przewidzianego do udowodnienia sposób rozwiązać z bardzo długim ciągiem, napisać kod, który może znaleźć jeden ciąg, który wychodzi każdy 3 przez 3 labirynt.

Z wyłączeniem nieprawidłowych labiryntów (rozpoczęcie i wyjście z tej samej komórki lub wyjście niedostępne od początku) istnieje 138 172 prawidłowych labiryntów i ciąg musi wyjść z każdego z nich.

Ważność

Ciąg musi spełniać następujące warunki:

  • Składa się tylko z liter „N”, „E”, „S” i „W”.
  • Wychodzi z dowolnego labiryntu, do którego jest stosowany, jeśli zaczyna się na początku.

Ponieważ zestaw wszystkich możliwych labiryntów obejmuje każdy możliwy labirynt z każdym możliwym ważnym punktem początkowym, oznacza to automatycznie, że łańcuch opuści każdy labirynt z dowolnego prawidłowego punktu początkowego. Oznacza to, że z dowolnego punktu początkowego, z którego można dotrzeć do wyjścia.

Zwycięski

Zwycięzca to odpowiedź, która zawiera najkrótszy prawidłowy ciąg i zawiera kod użyty do jego wytworzenia. Jeśli więcej niż jedna z odpowiedzi podaje ciąg o tej najkrótszej długości, wygrywa ten, który opublikuje ten ciąg.

Przykład

Oto przykładowy ciąg o długości 500 znaków, który da ci coś do pokonania:

SEENSSNESSWNNSNNNNWWNWENENNWEENSESSNENSESWENWWWWWENWNWWSESNSWENNWNWENWSSSNNNNNNESWNEWWWWWNNNSWESSEEWNENWENEENNEEESEENSSEENNWWWNWSWNSSENNNWESSESNWESWEENNWSNWWEEWWESNWEEEWWSSSESEEWWNSSEEEEESSENWWNNSWNENSESSNEESENEWSSNWNSEWEEEWEESWSNNNEWNNWNWSSWEESSSSNESESNENNWEESNWEWSWNSNWNNWENSNSWEWSWWNNWNSENESSNENEWNSSWNNEWSESWENEEENSWWSNNNNSSNENEWSNEEWNWENEEWEESEWEEWSSESSSWNWNNSWNWENWNENWNSWESNWSNSSENENNNWSSENSSSWWNENWWWEWSEWSNSSWNNSEWEWENSWENWSENEENSWEWSEWWSESSWWWNWSSEWSNWSNNWESNSNENNSNEWSNNESNNENWNWNNNEWWEWEE

Dziękujemy orlp za przekazanie tego.


Tabela liderów

Tabela liderów

Równe wyniki są wymienione w kolejności zamieszczania tego wyniku. Nie musi to być kolejność, w jakiej odpowiedzi zostały opublikowane, ponieważ wynik dla danej odpowiedzi może być z czasem aktualizowany.


Sędzia

Oto walidator Python 3, który pobiera ciąg NESW jako argument wiersza poleceń lub przez STDIN.

W przypadku niepoprawnego ciągu da to wizualny przykład labiryntu, dla którego zawodzi.

trichopaks
źródło
3
To naprawdę miłe pytanie. Czy istnieje jeden najkrótszy ciąg (lub kilka ciągów i dowód, że nie ma krótszych odpowiedzi)? A jeśli tak, to wiesz?
Alex Van Liew
1
@Alex Ponowne połączenie tak, początek może być dowolną z 9 komórek, a wyjściem może być dowolna z 9 komórek, o ile nie są one tą samą komórką, a wyjście jest osiągalne od początku.
trichoplax
1
Nieco podobne do pytania stackoverflow: stackoverflow.com/questions/26910401/... - ale komórka początkowa i końcowa znajdują się w lewym górnym i prawym dolnym rogu, co zmniejsza możliwą liczbę labiryntów do 2423.
schnaader
1
@proudhaskeller w obu przypadkach byłoby prawidłowym pytaniem. Ogólny przypadek, oceniany dla n = 3, wymagałby bardziej ogólnego kodu. Ten konkretny przypadek pozwala na optymalizacje, które nie dotyczą ogólnego n, i właśnie w ten sposób zdecydowałem się go zapytać.
trichoplax
2
Czy ktoś rozważał podejście do tego problemu jako znalezienie najkrótszego akceptowanego ciągu do wyrażenia regularnego? Wymagałoby to DUŻEGO zmniejszenia liczby problemów przed przejściem na wyrażenia regularne, ale teoretycznie mogłoby znaleźć możliwe do zweryfikowania optymalne rozwiązanie.
Kyle McCormick,

Odpowiedzi:

37

C ++, 97 95 93 91 86 83 82 81 79 znaków

NNWSWNNSENESESWSSWNSEENWWNWSSEWWNENWEENWSWNWSSENENWNWNESENESESWNWSESEWWNENWNEES

Moja strategia jest dość prosta - algorytm ewolucji, który może rosnąć, zmniejszać się, zamieniać elementy i mutować prawidłowe sekwencje. Moja logika ewolucji jest teraz prawie taka sama jak @ Sp3000, ponieważ była to poprawa w stosunku do mojej.

Jednak moja implementacja logiki labiryntu jest dość sprytna. To pozwala mi sprawdzić, czy ciągi są poprawne przy prędkości blisteringu. Spróbuj to rozgryźć, patrząc na komentarz do_movei Mazekonstruktora.

#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
źródło
5
Potwierdzony ważny. Jestem pod wrażeniem - nie spodziewałem się, że zobaczę tak krótkie napisy.
trichoplax
2
W końcu zabrałem się do instalowania gcc i uruchamiania tego dla siebie. Obserwowanie, jak struny mutują się i powoli kurczą, jest hipnotyczne ...
trichoplax
1
@trichoplax Mówiłem ci, że było fajnie :)
orlp
2
@AlexReinking Zaktualizowałem swoją odpowiedź ze wspomnianą implementacją. Jeśli spojrzysz na demontaż, zobaczysz, że to tylko tuzin instrukcji bez żadnego rozgałęzienia lub obciążenia: coliru.stacked-crooked.com/a/3b09d36db85ce793 .
orlp
2
@AlexReinking Done. do_movejest teraz niesamowicie szybki.
orlp
16

Python 3 + PyPy, 82 80 znaków

SWWNNSENESESWSSWSEENWNWSWSEWNWNENENWWSESSEWSWNWSENWEENWWNNESENESSWNWSESESWWNNESE

Wahałem się, czy opublikować tę odpowiedź, ponieważ w zasadzie podjąłem podejście orlp i rzuciłem na to swój własny obrót. Ten ciąg został znaleziony, zaczynając od pseudolosowego rozwiązania o długości 500 - spora liczba nasion została wypróbowana, zanim udało mi się pobić aktualny rekord.

Jedyną nową dużą optymalizacją jest to, że patrzę tylko na jedną trzecią labiryntów. Dwie kategorie labiryntów są wyłączone z wyszukiwania:

  • Labirynty, w których <= 7dostępne są kwadraty
  • Labirynty, w których wszystkie osiągalne kwadraty znajdują się na jednej ścieżce, a początek / koniec nie znajdują się na obu końcach

Chodzi o to, że każdy ciąg rozwiązujący pozostałe labirynty powinien również rozwiązać powyższe automatycznie. Jestem przekonany, że jest to prawdą w przypadku drugiego typu, ale zdecydowanie nie jest prawdą w przypadku pierwszego, więc dane wyjściowe będą zawierały fałszywe alarmy, które należy sprawdzić osobno. Tym fałszywym pozytywom zwykle brakuje tylko około 20 labiryntów, więc pomyślałem, że to dobry kompromis między szybkością i dokładnością, a także dałoby strunie nieco więcej przestrzeni do mutacji.

Początkowo przeglądałem długą listę heurystyk wyszukiwania, ale przerażająco żadne z nich nie wymyśliło niczego lepszego niż około 140.

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
źródło
Potwierdzony ważny. Ładne ulepszenia :)
trichoplax
Podoba mi się twój pomysł, że niektóre labirynty nie muszą być sprawdzane. Czy mógłbyś w jakiś sposób zautomatyzować proces określania, które labirynty są zbędnymi kontrolami? Ciekawe, czy to pokazałoby więcej labiryntów niż te, które można intuicyjnie wydedukować ...
trichoplax
Jaki jest twój powód, dla którego nie musisz sprawdzać wykresów ścieżek, w których początek nie jest na jednym końcu? Przypadek, w którym wykończenie nie jest na jednym końcu, jest łatwy do uzasadnienia i można go wzmocnić tak, aby nie musiał sprawdzać przypadków, w których wykończenie jest wyciętym wierzchołkiem, ale nie widzę, jak uzasadnić wyeliminowanie wierzchołków początkowych.
Peter Taylor,
@PeterTaylor Po dłuższym zastanowieniu, teoretycznie masz rację, jest kilka labiryntów, których nie możesz tak wyeliminować. Wydaje się jednak, że na 3x3 tak długo nie ma to znaczenia dla łańcuchów.
orlp
2
@orlp, Sp3000 naszkicował dowód na czacie. Wykresy ścieżek są szczególnym przypadkiem. Przenumerować komórki 0na nścieżce i załóżmy, że ciąg Sdostaje od 0celu n. Następnie Sprzenosi cię z dowolnej komórki pośredniej cdo n. Załóżmy inaczej. Niech a(i)będzie pozycja po ikrokach zaczynających się od 0i b(i)zaczynających się od c. Następnie a(0) = 0 < b(0), każdy krok zmienia się ai bnajwyżej o 1, i a(|S|) = n > b(|S|). Weź najmniejszy ttaki, że a(t) >= b(t). Najwyraźniej a(t) != b(t)byłyby zsynchronizowane, więc muszą zamieniać miejsca krok po kroku t, poruszając się w tym samym kierunku.
Peter Taylor,
3

C ++ i biblioteka z lingelingiem

Podsumowanie: Nowe podejście, brak nowych rozwiązań , fajny program do zabawy i kilka interesujących rezultatów lokalnej niemożliwości poprawy znanych rozwiązań. Aha, i kilka ogólnie przydatnych obserwacji.

Stosując podejście oparte na SAT , mogłem całkowicie rozwiązać podobny problem dla labiryntów 4x4 z zablokowanymi komórkami zamiast cienkich ścian i ze stałymi pozycjami początkowymi i wyjściowymi w przeciwległych rogach. Miałem więc nadzieję, że uda mi się wykorzystać te same pomysły do ​​rozwiązania tego problemu. Jednakże, chociaż dla drugiego problemu użyłem tylko 2423 labiryntów (w międzyczasie zaobserwowano, że 2083 wystarczy) i ma rozwiązanie o długości 29, kodowanie SAT wykorzystało miliony zmiennych, a jego rozwiązanie zajęło kilka dni.

Postanowiłem więc zmienić podejście na dwa ważne sposoby:

  • Nie nalegaj na wyszukiwanie rozwiązania od zera, ale pozwól naprawić część ciągu rozwiązania. (W każdym razie łatwo to zrobić, dodając klauzule jednostki, ale mój program sprawia, że ​​jest to wygodne.)
  • Nie używaj wszystkich labiryntów od samego początku. Zamiast tego stopniowo dodawaj jeden nierozwiązany labirynt naraz. Niektóre labirynty mogą być rozwiązane przypadkowo lub zawsze są rozwiązywane, gdy te już rozważone zostaną rozwiązane. W tym drugim przypadku nigdy nie zostanie dodany, bez naszej wiedzy na temat konsekwencji.

Zrobiłem także pewne optymalizacje, aby użyć mniej zmiennych i klauzul jednostkowych.

Program oparty jest na @ orlp's. Ważną zmianą był wybór labiryntów:

  • Przede wszystkim labirynty są podawane tylko przez strukturę ściany i pozycję początkową. (Przechowują również pozycje osiągalne.) Funkcja is_solutionsprawdza, czy wszystkie osiągalne pozycje są osiągnięte.
  • (Bez zmian: nadal nie używają labiryntów z tylko 4 lub mniej dostępnymi pozycjami. Ale większość z nich i tak zostanie wyrzucona przez następujące obserwacje).
  • Jeśli labirynt nie używa żadnej z trzech górnych komórek, jest to odpowiednik przesuniętego w górę labiryntu. Więc możemy to upuścić. Podobnie w przypadku labiryntu, który nie wykorzystuje żadnej z trzech lewych komórek.
  • Nie ma znaczenia, czy nieosiągalne części są połączone, dlatego nalegamy, aby każda nieosiągalna komórka była całkowicie otoczona ścianami.
  • Labirynt z pojedynczą ścieżką, który jest submaze większego labiryntu z pojedynczą ścieżką, jest zawsze rozwiązywany, gdy większy jest rozwiązywany, więc nie potrzebujemy go. Każdy labirynt pojedynczej ścieżki o wielkości maksymalnie 7 jest częścią większego (wciąż mieszczącego się w rozmiarze 3 x 3), ale istnieją labirynty o pojedynczej ścieżce wielkości 8, które nie są. Upraszczając, po prostu upuśćmy labirynty z pojedynczą ścieżką o rozmiarze mniejszym niż 8. (I nadal używam tego, że tylko skrajne punkty muszą być uważane za pozycje początkowe. Wszystkie pozycje są używane jako pozycje wyjściowe, co ma znaczenie tylko dla części SAT programu).

W ten sposób otrzymuję 10772 labiryntów z pozycjami początkowymi.

Oto program:

#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);
  }
}  

Pierwszy configure.shi Solver, a następnie skompilować program ze coś takiego , gdzie jest ścieżka gdzie resp. są, więc oba mogą być na przykład . Lub po prostu umieść je w tym samym katalogu i nie używaj opcji i .makelingelingg++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl...lglib.hliblgl.a../lingeling-<version>-I-L

Program trwa jedną obowiązkową wiersza poleceń argumentu ciąg składający się z N, E, S, W(dla stałych kierunków) lub *. Możesz więc wyszukać ogólne rozwiązanie o rozmiarze 78, podając ciąg 78 *s (w cudzysłowach), lub wyszukaj rozwiązanie, zaczynając NEWSod NEWSpoprzedzających go tylu, *ile chcesz , aby wykonać dodatkowe kroki. Jako pierwszy test weź swoje ulubione rozwiązanie i zamień niektóre litery na *. Szybko znajduje to rozwiązanie dla zaskakująco wysokiej wartości „niektórych”.

Program powie, który labirynt dodaje, opisany przez strukturę ściany i pozycję początkową, a także poda liczbę dostępnych pozycji i ścian. Labirynty są sortowane według tych kryteriów i dodawany jest pierwszy nierozwiązany. Dlatego większość dodanych labiryntów ma (9/4), ale czasami pojawiają się również inne.

Wziąłem znane rozwiązanie o długości 79 i dla każdej grupy sąsiadujących 26 liter próbowałem zastąpić je dowolnymi 25 literami. Próbowałem także usunąć 13 liter od początku i od końca i zastąpić je dowolnymi 13 na początku i dowolnymi 12 na końcu i odwrotnie. Niestety wszystko wyszło niezadowalająco. Czy możemy zatem uznać to za wskaźnik, że długość 79 jest optymalna? Nie, podobnie próbowałem ulepszyć rozwiązanie o długości 80 do długości 79, ale to też nie powiodło się.

Wreszcie próbowałem połączyć początek jednego rozwiązania z końcem drugiego, a także z jednym rozwiązaniem przekształconym przez jedną z symetrii. Teraz brakuje mi ciekawych pomysłów, więc postanowiłem pokazać ci, co mam, mimo że nie doprowadziło to do nowych rozwiązań.

Christian Sievers
źródło
To była naprawdę interesująca lektura. Zarówno nowe podejście, jak i różne sposoby zmniejszania liczby labiryntów do sprawdzenia. Aby być poprawną odpowiedzią, musi zawierać poprawny ciąg. Nie musi to być nowy najkrótszy ciąg, tylko dowolny prawidłowy ciąg dowolnej długości, aby dać aktualny wynik dla tego podejścia. Wspominam o tym, ponieważ bez wyniku odpowiedź będzie zagrożona usunięciem i naprawdę chciałbym, aby pozostała.
trichoplax
Również dobra praca nad znalezieniem optymalnego rozwiązania długości dla starszego pokrewnego wyzwania !
trichoplax