Corda di uscita labirinto universale più corta


48

Un labirinto su una griglia N per N di celle quadrate viene definito specificando se ciascun bordo è un muro o meno. Tutti i bordi esterni sono pareti. Una cella viene definita come inizio e una cella viene definita come uscita e l'uscita è raggiungibile dall'inizio. L'inizio e l'uscita non sono mai la stessa cella.

Nota che né l'inizio né l'uscita devono trovarsi sul bordo esterno del labirinto, quindi questo è un labirinto valido:

Un labirinto 3 per 3 con l'uscita sulla cella centrale

Una stringa di "N", "E", "S" e "W" indica il tentativo di spostarsi rispettivamente verso nord, est, sud e ovest. Una mossa bloccata da un muro viene saltata senza movimento. Una stringa esce da un labirinto se l'applicazione di quella stringa dall'inizio provoca il raggiungimento dell'uscita (indipendentemente dal fatto che la stringa continui dopo aver raggiunto l'uscita).

Ispirato da questa domanda enigmistica.SE per la quale xnor ha fornito un metodo dimostrabile di risoluzione con una stringa molto lunga, scrivi il codice che può trovare una singola stringa che esce da un labirinto 3 per 3.

Escludendo labirinti non validi (inizio e uscita sulla stessa cella o uscita non raggiungibile dall'inizio) ci sono 138.172 labirinti validi e la stringa deve uscire da ciascuno di essi.

Validità

La stringa deve soddisfare quanto segue:

  • È composto solo dai caratteri "N", "E", "S" e "W".
  • Esce da qualsiasi labirinto a cui è applicato, se avviato all'inizio.

Poiché l'insieme di tutti i possibili labirinti include ogni possibile labirinto con ogni possibile punto di partenza valido, ciò significa automaticamente che la stringa uscirà da qualsiasi labirinto da qualsiasi punto di partenza valido. Cioè, da qualsiasi punto di partenza da cui è raggiungibile l'uscita.

vincente

Il vincitore è la risposta che fornisce la stringa valida più breve e include il codice utilizzato per produrla. Se più di una delle risposte fornisce una stringa di questa lunghezza più breve, vince la prima a pubblicare quella lunghezza di stringa.

Esempio

Ecco una stringa di esempio lunga 500 caratteri, per darti qualcosa da battere:

SEENSSNESSWNNSNNNNWWNWENENNWEENSESSNENSESWENWWWWWENWNWWSESNSWENNWNWENWSSSNNNNNNESWNEWWWWWNNNSWESSEEWNENWENEENNEEESEENSSEENNWWWNWSWNSSENNNWESSESNWESWEENNWSNWWEEWWESNWEEEWWSSSESEEWWNSSEEEEESSENWWNNSWNENSESSNEESENEWSSNWNSEWEEEWEESWSNNNEWNNWNWSSWEESSSSNESESNENNWEESNWEWSWNSNWNNWENSNSWEWSWWNNWNSENESSNENEWNSSWNNEWSESWENEEENSWWSNNNNSSNENEWSNEEWNWENEEWEESEWEEWSSESSSWNWNNSWNWENWNENWNSWESNWSNSSENENNNWSSENSSSWWNENWWWEWSEWSNSSWNNSEWEWENSWENWSENEENSWEWSEWWSESSWWWNWSSEWSNWSNNWESNSNENNSNEWSNNESNNENWNWNNNEWWEWEE

Grazie a orlp per aver donato questo.


Classifica

Classifica

I punteggi uguali sono elencati in ordine di pubblicazione di quel punteggio. Questo non è necessariamente l'ordine in cui le risposte sono state pubblicate poiché il punteggio per una determinata risposta può essere aggiornato nel tempo.


Giudice

Ecco un validatore di Python 3 che accetta una stringa di NESW come argomento della riga di comando o tramite STDIN.

Per una stringa non valida, questo ti darà un esempio visivo di un labirinto per cui non riesce.


3
Questa è una domanda davvero chiara. Esiste una stringa più breve (o un numero di stringhe e una prova che non ci possono essere risposte più brevi)? E se è così, lo sai?
Alex Van Liew,

1
@AlexReinking sì, l'inizio può essere una qualsiasi delle 9 celle e l'uscita può essere una qualsiasi delle 9 celle, purché non siano la stessa cella e l'uscita è raggiungibile dall'inizio.
trichoplax,

1
Leggermente simile a questa domanda StackOverflow: stackoverflow.com/questions/26910401/... - ma inizio e fine sono cellule superiore sinistra e destra inferiore in quella, che riduce l'eventuale conteggio labirinto per 2423.
schnaader

1
@proudhaskeller in entrambi i casi sarebbe una domanda valida. Il caso generale, segnato per n = 3, richiederebbe un codice più generalizzato. Questo caso specifico consente ottimizzazioni che non si applicano al generale n, ed è così che ho scelto di chiederlo.
trichoplax,

2
Qualcuno ha considerato di affrontare questo problema come trovare la stringa accettata più breve ad un'espressione regolare? Richiederebbe MOLTE riduzioni del numero di problemi prima della conversione in regex, ma potrebbe teoricamente trovare una soluzione verificabile in modo ottimale.
Kyle McCormick,

Risposte:


37

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

NNWSWNNSENESESWSSWNSEENWWNWSSEWWNENWEENWSWNWSSENENWNWNESENESESWNWSESEWWNENWNEES

La mia strategia è abbastanza semplice: un algoritmo di evoluzione che può crescere, restringere, scambiare elementi e mutare sequenze valide. La mia logica di evoluzione ora è quasi la stessa di @ Sp3000, in quanto il suo era un miglioramento rispetto al mio.

Tuttavia, la mia implementazione della logica del labirinto è piuttosto ingegnosa. Questo mi permette di verificare se le stringhe sono valide alla velocità della vescica. Prova a capirlo guardando il commento do_movee il Mazecostruttore.

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

5
Confermato valido. Sono impressionato - non mi aspettavo di vedere stringhe così brevi.
trichoplax,

2
Alla fine ho iniziato a installare gcc e ad eseguirlo da solo. È ipnotico guardare le corde mutare e lentamente restringersi ...
trichoplax,

1
@trichoplax Ti ho detto che è stato divertente :)
orlp

2
@AlexReinking Ho aggiornato la mia risposta con detta implementazione. Se guardi lo smontaggio vedrai che sono solo una dozzina di istruzioni senza alcun ramo o carico: coliru.stacked-crooked.com/a/3b09d36db85ce793 .
orlp

2
@AlexReinking Done. do_moveè ora follemente veloce.
orlp

16

Python 3 + PyPy, 82 80 caratteri

SWWNNSENESESWSSWSEENWNWSWSEWNWNENENWWSESSEWSWNWSENWEENWWNNESENESSWNWSESESWWNNESE

Sono stato titubante nel pubblicare questa risposta perché ho sostanzialmente adottato l'approccio di orlp e ci ho messo il mio giro. Questa stringa è stata trovata iniziando con una soluzione di lunghezza pseudocasuale di 500 - un bel po 'di semi sono stati provati prima che potessi battere il record attuale.

L'unica nuova grande ottimizzazione è che guardo solo un terzo dei labirinti. Due categorie di labirinti sono esclusi dalla ricerca:

  • Labirinti dove <= 7sono raggiungibili i quadrati
  • Labirinti in cui tutti i quadrati raggiungibili si trovano su un unico percorso e l'inizio / fine non sono ad entrambe le estremità

L'idea è che qualsiasi stringa che risolva il resto dei labirinti dovrebbe anche risolvere automaticamente quanto sopra. Sono convinto che questo sia vero per il secondo tipo, ma sicuramente non è vero per il primo, quindi l'output conterrà alcuni falsi positivi che devono essere controllati separatamente. Questi falsi positivi di solito mancano solo circa 20 labirinti, quindi ho pensato che sarebbe stato un buon compromesso tra velocità e precisione, e avrebbe anche dato alle corde un po 'più di respiro per mutare.

Inizialmente ho attraversato un lungo elenco di euristiche di ricerca, ma in modo orribile nessuno di loro ha trovato qualcosa di meglio di 140 o giù di lì.

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

Confermato valido.
Ottimi

Mi piace che la tua idea di realizzare alcuni labirinti non abbia bisogno di essere controllata. Potresti in qualche modo automatizzare il processo per determinare quali labirinti sono controlli ridondanti? Sono curioso di sapere se ciò mostrerebbe più labirinti di quelli che si possono dedurre intuitivamente ...
trichoplax

Qual è il tuo ragionamento per non aver bisogno di controllare i grafici dei percorsi in cui l'inizio non è ad una fine? Il caso in cui la finitura non è ad una estremità è facile da giustificare e può essere rafforzato per non dover controllare i casi in cui la finitura è un vertice tagliato, ma non riesco a vedere come giustificare l'eliminazione dei vertici di partenza.
Peter Taylor,

@PeterTaylor Dopo qualche altra riflessione, teoricamente hai ragione, ci sono alcuni labirinti che non puoi eliminare in quel modo. Tuttavia, sembra che su 3x3 non abbia importanza per le stringhe così a lungo.
orlp

2
@orlp, Sp3000 ha disegnato una prova in chat. I grafici dei percorsi sono un caso speciale. Rinumerare le cellule 0a nlungo il percorso e supponiamo che stringa Ssi ottiene da 0a n. Quindi Sti porta anche da qualsiasi cella intermedia ca n. Supponiamo altrimenti. Lascia che a(i)sia la posizione dopo i ipassaggi che iniziano 0e b(i)iniziano a c. Quindi a(0) = 0 < b(0), ogni passaggio cambia ae bal massimo 1, e a(|S|) = n > b(|S|). Prendi il più piccolo in tmodo tale a(t) >= b(t). Chiaramente a(t) != b(t)o sarebbero sincronizzati, quindi devono scambiare i posti al passo tmuovendosi nella stessa direzione.
Peter Taylor,

3

C ++ e la libreria dal persistere

Riepilogo: un nuovo approccio, nessuna nuova soluzione , un bel programma con cui giocare e alcuni risultati interessanti della non improvvisabilità locale delle soluzioni conosciute. Oh, e alcune osservazioni generalmente utili.

Utilizzando un approccio basato su SAT , sono stato in grado di risolvere completamente il problema simile per i labirinti 4x4 con celle bloccate anziché pareti sottili e posizioni di partenza e uscita fisse agli angoli opposti. Quindi speravo di poter usare le stesse idee per questo problema. Tuttavia, anche se per l'altro problema ho usato solo 2423 labirinti (nel frattempo è stato osservato che 2083 sono sufficienti) e ha una soluzione di lunghezza 29, la codifica SAT ha utilizzato milioni di variabili e la risoluzione ha richiesto giorni.

Quindi ho deciso di cambiare l'approccio in due modi importanti:

  • Non insistere sulla ricerca di una soluzione da zero, ma consentire di correggere una parte della stringa della soluzione. (È facile da fare comunque aggiungendo clausole unitarie, ma il mio programma lo rende comodo da fare.)
  • Non usare tutti i labirinti dall'inizio. Invece, aggiungi in modo incrementale un labirinto irrisolto alla volta. Alcuni labirinti possono essere risolti per caso, oppure sono sempre risolti quando quelli già considerati sono risolti. In quest'ultimo caso, non verrà mai aggiunto, senza che noi abbiamo bisogno di conoscere le implicazioni.

Ho anche fatto alcune ottimizzazioni per usare meno variabili e clausole unitarie.

Il programma si basa su @ orlp's. Un cambiamento importante è stata la selezione di labirinti:

  • Prima di tutto, i labirinti sono dati dalla loro struttura del muro e solo dalla posizione iniziale. (Memorizzano anche le posizioni raggiungibili.) La funzione is_solutioncontrolla se tutte le posizioni raggiungibili sono raggiunte.
  • (Invariato: continua a non usare labirinti con solo 4 o meno posizioni raggiungibili. Ma la maggior parte di essi verrebbe comunque buttata via dalle seguenti osservazioni.)
  • Se un labirinto non utilizza nessuna delle tre celle superiori, è equivalente a un labirinto spostato verso l'alto. Quindi possiamo lasciarlo cadere. Allo stesso modo per un labirinto che non utilizza nessuna delle tre celle a sinistra.
  • Non importa se le parti non raggiungibili sono collegate, quindi insistiamo sul fatto che ogni cella non raggiungibile è completamente circondata da muri.
  • Un labirinto a percorso singolo che è un sottomarino di un labirinto a percorso singolo più grande viene sempre risolto quando viene risolto quello più grande, quindi non ne abbiamo bisogno. Ogni labirinto a singolo percorso di dimensioni al massimo 7 fa parte di un labirinto più grande (ancora adatto a 3x3), ma ci sono labirinti a percorso singolo di dimensioni 8 che non lo sono. Per semplicità, lasciamo cadere labirinti a percorso singolo di dimensioni inferiori a 8. (E sto ancora usando che solo i punti estremi devono essere considerati come posizioni di partenza. Tutte le posizioni sono usate come posizioni di uscita, che conta solo per la parte SAT del programma.)

In questo modo, ottengo un totale di 10772 labirinti con posizioni iniziali.

Ecco il programma:

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

Prima configure.she makeil lingelingrisolutore, quindi compilare il programma con qualcosa di simile g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl, dov'è ...il percorso in cui lglib.hresp. liblgl.alo sono, quindi entrambi potrebbero essere ad esempio ../lingeling-<version>. O semplicemente inseriscili nella stessa directory e fai a meno delle opzioni -Ie -L.

Il programma prende un argomento obbligatorio linea di comando, una stringa costituita N, E, S, W(per direzioni fisse) o *. Quindi potresti cercare una soluzione generale della dimensione 78 fornendo una stringa di 78 *s (tra virgolette) o cercare una soluzione che inizia con l' NEWSuso NEWSseguito da tutti gli *s che desideri per ulteriori passaggi. Come primo test, prendi la tua soluzione preferita e sostituisci alcune delle lettere con *. Questo trova una soluzione veloce per un valore sorprendentemente alto di "alcuni".

Il programma dirà quale labirinto aggiunge, descritto dalla struttura della parete e dalla posizione iniziale e fornirà anche il numero di posizioni e pareti raggiungibili. I labirinti sono ordinati in base a questi criteri e viene aggiunto il primo irrisolto. Pertanto la maggior parte dei labirinti aggiunti ha (9/4), ma a volte ne appaiono anche altri.

Ho preso la soluzione nota della lunghezza 79 e, per ogni gruppo di 26 lettere adiacenti, ho provato a sostituirle con 25 lettere qualsiasi. Ho anche provato a rimuovere 13 lettere dall'inizio e dalla fine e sostituirle con 13 all'inizio e 12 alla fine e viceversa. Sfortunatamente, tutto è risultato insoddisfacente. Quindi, possiamo prendere questo come indicatore che la lunghezza 79 è ottimale? No, ho anche cercato di migliorare la soluzione lunghezza 80 alla lunghezza 79, e anche questo non ha avuto successo.

Alla fine, ho provato a combinare l'inizio di una soluzione con la fine dell'altra, e anche con una soluzione trasformata da una delle simmetrie. Ora sto esaurendo le idee interessanti, quindi ho deciso di mostrarti quello che ho, anche se non ha portato a nuove soluzioni.


È stata una lettura davvero interessante. Sia il nuovo approccio che i diversi modi di ridurre il numero di labirinti da controllare. Per essere una risposta valida, sarà necessario includere una stringa valida. Non è necessario che sia una nuova stringa più breve, ma solo una stringa valida di qualsiasi lunghezza per ottenere un punteggio corrente per questo approccio. Lo menziono perché senza un punteggio, la risposta sarà a rischio di eliminazione e mi piacerebbe davvero vederla rimanere.
trichoplax,

Anche un bel lavoro per trovare la soluzione di lunghezza ottimale per la sfida più vecchia !
trichoplax
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.