Cadena de salida de laberinto universal más corta

48

Un laberinto en una cuadrícula de celdas cuadradas N por N se define al especificar si cada borde es un muro o no un muro. Todos los bordes exteriores son paredes. Una celda se define como el inicio , y una celda se define como la salida , y la salida es accesible desde el inicio. El inicio y la salida nunca son la misma celda.

Tenga en cuenta que ni el inicio ni la salida deben estar en el borde exterior del laberinto, por lo que este es un laberinto válido:

Un laberinto de 3 por 3 con la salida en la celda central

Una cadena de 'N', 'E', 'S' y 'W' indica que intenta moverse hacia el norte, este, sur y oeste, respectivamente. Un movimiento que está bloqueado por un muro se omite sin movimiento. Una cadena sale de un laberinto si la aplicación de esa cadena desde el inicio da como resultado que se alcance la salida (independientemente de si la cadena continúa después de llegar a la salida).

Inspirado por esta pregunta enigmática. SE para la cual xnor proporcionó un método comprobable de resolución con una cadena muy larga, escriba código que pueda encontrar una sola cadena que salga de un laberinto de 3 por 3.

Excluyendo laberintos inválidos (inicio y salida en la misma celda, o salida no accesible desde el inicio) hay 138,172 laberintos válidos y la cadena debe salir de cada uno de ellos.

Validez

La cadena debe cumplir lo siguiente:

  • Está compuesto solo por los caracteres 'N', 'E', 'S' y 'W'.
  • Sale de cualquier laberinto al que se aplica, si se inicia desde el principio.

Dado que el conjunto de todos los laberintos posibles incluye cada laberinto posible con cada punto de inicio válido posible, esto significa automáticamente que la cadena saldrá de cualquier laberinto desde cualquier punto de inicio válido. Es decir, desde cualquier punto de partida desde el que se pueda alcanzar la salida.

Victorioso

El ganador es la respuesta que proporciona la cadena válida más corta e incluye el código utilizado para producirla. Si más de una de las respuestas proporciona una cadena de esta longitud más corta, gana el primero en publicar esa longitud de cadena.

Ejemplo

Aquí hay una cadena de ejemplo de 500 caracteres de longitud, para darle algo que vencer:

SEENSSNESSWNNSNNNNWWNWENENNWEENSESSNENSESWENWWWWWENWNWWSESNSWENNWNWENWSSSNNNNNNESWNEWWWWWNNNSWESSEEWNENWENEENNEEESEENSSEENNWWWNWSWNSSENNNWESSESNWESWEENNWSNWWEEWWESNWEEEWWSSSESEEWWNSSEEEEESSENWWNNSWNENSESSNEESENEWSSNWNSEWEEEWEESWSNNNEWNNWNWSSWEESSSSNESESNENNWEESNWEWSWNSNWNNWENSNSWEWSWWNNWNSENESSNENEWNSSWNNEWSESWENEEENSWWSNNNNSSNENEWSNEEWNWENEEWEESEWEEWSSESSSWNWNNSWNWENWNENWNSWESNWSNSSENENNNWSSENSSSWWNENWWWEWSEWSNSSWNNSEWEWENSWENWSENEENSWEWSEWWSESSWWWNWSSEWSNWSNNWESNSNENNSNEWSNNESNNENWNWNNNEWWEWEE

Gracias a orlp por donar esto.


Tabla de clasificación

Tabla de clasificación

Los puntajes iguales se enumeran en orden de publicación de ese puntaje. Este no es necesariamente el orden en que se publicaron las respuestas, ya que la puntuación de una respuesta determinada puede actualizarse con el tiempo.


Juez

Aquí hay un validador de Python 3 que toma una cadena de NESW como argumento de línea de comando o mediante STDIN.

Para una cadena no válida, esto le dará un ejemplo visual de un laberinto en el que falla.

trichoplax
fuente
3
Esta es una pregunta realmente buena. ¿Hay una cadena más corta (o varias cadenas y una prueba de que no puede haber respuestas más cortas)? Y si es así, ¿lo sabes?
Alex Van Liew
1
@AlexReinking sí, el inicio puede ser cualquiera de las 9 celdas y la salida puede ser cualquiera de las 9 celdas, siempre que no sean la misma celda y la salida sea accesible desde el inicio.
trichoplax
1
Ligeramente similar a esta pregunta de stackoverflow: stackoverflow.com/questions/26910401/… - pero las celdas de inicio y fin están arriba a la izquierda y abajo a la derecha en esa, lo que reduce el posible recuento de laberintos a 2423.
schnaader
1
@proudhaskeller de cualquier manera sería una pregunta válida. El caso general, calificado para n = 3, requeriría un código más generalizado. Este caso específico permite optimizaciones que no se aplican al n general, y esa es la forma en que elegí preguntarlo.
trichoplax
2
¿Alguien ha considerado abordar este problema como encontrar la cadena más corta aceptada para una expresión regular? Requeriría MUCHAS reducciones en el número de problemas antes de convertir a expresiones regulares, pero en teoría podría encontrar una solución óptimamente verificable.
Kyle McCormick el

Respuestas:

37

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

NNWSWNNSENESESWSSWNSEENWWNWSSEWWNENWEENWSWNWSSENENWNWNESENESESWNWSESEWWNENWNEES

Mi estrategia es bastante simple: un algoritmo de evolución que puede crecer, reducir, intercambiar elementos y mutar secuencias válidas. Mi lógica de evolución ahora es casi la misma que la de @ Sp3000, ya que la suya fue una mejora sobre la mía.

Sin embargo, mi implementación de la lógica del laberinto es bastante ingeniosa. Esto me permite verificar si las cadenas son válidas a una velocidad vertiginosa. Intenta resolverlo mirando el comentario do_movey el Mazeconstructor.

#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
fuente
55
Confirmado válido. Estoy impresionado, no esperaba ver cadenas tan cortas.
trichoplax
2
Finalmente pude instalar gcc y ejecutar esto por mí mismo. Es hipnótico ver las cuerdas mutando y encogiéndose lentamente ...
trichoplax
1
@trichoplax Te dije que fue divertido :)
orlp
2
@AlexReinking Actualicé mi respuesta con dicha implementación. Si observa el desmontaje, verá que son solo una docena de instrucciones sin ninguna rama o carga: coliru.stacked-crooked.com/a/3b09d36db85ce793 .
orlp
2
@AlexReinking Hecho. do_moveahora es increíblemente rápido.
orlp
16

Python 3 + PyPy, 82 80 caracteres

SWWNNSENESESWSSWSEENWNWSWSEWNWNENENWWSESSEWSWNWSENWEENWWNNESENESSWNWSESESWWNNESE

Dudé en publicar esta respuesta porque básicamente tomé el enfoque de orlp y le di mi propio giro. Esta cadena se encontró comenzando con una solución pseudoaleatoria de longitud 500: se probaron bastantes semillas antes de que pudiera romper el récord actual.

La única nueva optimización importante es que solo miro un tercio de los laberintos. Se excluyen dos categorías de laberintos de la búsqueda:

  • Laberintos donde los <= 7cuadrados son accesibles
  • Laberintos donde todos los cuadrados accesibles están en un solo camino, y el inicio / final no están en ambos extremos

La idea es que cualquier cadena que resuelva el resto de los laberintos también debe resolver lo anterior automáticamente. Estoy convencido de que esto es cierto para el segundo tipo, pero definitivamente no lo es para el primero, por lo que la salida contendrá algunos falsos positivos que deben verificarse por separado. Sin embargo, estos falsos positivos generalmente solo pierden unos 20 laberintos, así que pensé que sería una buena compensación entre velocidad y precisión, y también les daría a las cuerdas un poco más de espacio para respirar para mutar.

Inicialmente, revisé una larga lista de heurísticas de búsqueda, pero horriblemente ninguno de ellos obtuvo nada mejor que 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
fuente
Confirmado válido. Bonitas mejoras :)
trichoplax
Me gusta su idea de darse cuenta de que algunos laberintos no necesitan ser revisados. ¿Podría de alguna manera automatizar el proceso de determinar qué laberintos son controles redundantes? Tengo curiosidad por saber si eso mostraría más laberintos que los que se pueden deducir intuitivamente ...
trichoplax
¿Cuál es su razonamiento para no necesitar verificar gráficos de ruta donde el inicio no está en un extremo? El caso en el que el acabado no está en un extremo es fácil de justificar, y puede fortalecerse para no tener que verificar los casos en los que el acabado es un vértice cortado, pero no puedo ver cómo justificar la eliminación de los vértices de inicio.
Peter Taylor
@PeterTaylor Después de pensar un poco más, teóricamente tienes razón, hay algunos laberintos que no puedes eliminar así. Sin embargo, parece que en 3x3 no importa para cadenas tan largas.
orlp
2
@orlp, Sp3000 esbozó una prueba en el chat. Los gráficos de ruta son un caso especial. Vuelva a numerar las celdas 0a lo nlargo del camino y suponga que esa cadena lo Slleva de 0a n. Luego Stambién te lleva de cualquier celda intermedia ca n. Supongamos lo contrario. Deje a(i)ser la posición después de los ipasos que comienzan 0y b(i)comienzan en c. Luego a(0) = 0 < b(0), cada paso cambia ay bcomo máximo 1, y a(|S|) = n > b(|S|). Toma la más pequeña de tesa manera a(t) >= b(t). Claramente a(t) != b(t)o estarían sincronizados, por lo que deben intercambiar lugares paso a paso tmoviéndose en la misma dirección.
Peter Taylor
3

C ++ y la biblioteca de lingeling

Resumen: un nuevo enfoque, sin nuevas soluciones , un buen programa para jugar y algunos resultados interesantes de la no mejorabilidad local de las soluciones conocidas. Ah, y algunas observaciones generalmente útiles.

Utilizando un enfoque basado en SAT , pude resolver completamente el problema similar para laberintos 4x4 con celdas bloqueadas en lugar de paredes delgadas y posiciones fijas de inicio y salida en esquinas opuestas. Así que esperaba poder usar las mismas ideas para este problema. Sin embargo, aunque para el otro problema solo usé 2423 laberintos (mientras tanto se ha observado que 2083 son suficientes) y tiene una solución de longitud 29, la codificación SAT usó millones de variables y su resolución tomó días.

Así que decidí cambiar el enfoque de dos maneras importantes:

  • No insista en buscar una solución desde cero, pero permita arreglar una parte de la cadena de la solución. (Eso es fácil de hacer de todos modos agregando cláusulas de unidad, pero mi programa lo hace cómodo).
  • No uses todos los laberintos desde el principio. En cambio, agregue incrementalmente un laberinto sin resolver a la vez. Algunos laberintos pueden resolverse por casualidad, o siempre se resuelven cuando se resuelven los que ya se consideran. En el último caso, nunca se agregará, sin que necesitemos saber la implicación.

También hice algunas optimizaciones para usar menos variables y cláusulas unitarias.

El programa se basa en @ orlp's. Un cambio importante fue la selección de laberintos:

  • En primer lugar, los laberintos están dados únicamente por su estructura de pared y la posición de inicio. (También almacenan las posiciones alcanzables.) La función is_solutionverifica si se alcanzan todas las posiciones alcanzables.
  • (Sin cambios: todavía no se utilizan laberintos con solo 4 o menos posiciones alcanzables. Pero la mayoría de ellos serían desechados de todos modos por las siguientes observaciones).
  • Si un laberinto no usa ninguna de las tres celdas superiores, es equivalente a un laberinto desplazado hacia arriba. Entonces podemos dejarlo caer. Del mismo modo para un laberinto que no utiliza ninguna de las tres celdas de la izquierda.
  • No importa si hay partes inalcanzables conectadas, por lo que insistimos en que cada celda inalcanzable esté completamente rodeada por paredes.
  • Un laberinto de un solo camino que es un submaze de un laberinto de un solo camino más grande siempre se resuelve cuando se resuelve el laberinto más grande, por lo que no lo necesitamos. Cada laberinto de ruta individual de tamaño máximo 7 es parte de uno más grande (que todavía se ajusta en 3x3), pero hay laberintos de ruta única de tamaño 8 que no lo son. Para simplificar, dejemos caer laberintos de un solo camino de tamaño inferior a 8. (Y sigo usando que solo los puntos extremos deben considerarse como posiciones de inicio. Todas las posiciones se usan como posiciones de salida, lo que solo importa para la parte SAT Del programa.)

De esta manera, obtengo un total de 10772 laberintos con posiciones de inicio.

Aquí está el programa:

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

Primero configure.shy makeel lingelingsolucionador, luego compila el programa con algo como g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl, dónde ...está el camino donde lglib.hresp. liblgl.ason, por lo que ambos podrían ser, por ejemplo ../lingeling-<version>. O simplemente colóquelos en el mismo directorio y no use las opciones -Iy -L.

El programa toma un argumento de línea de comandos obligatoria, una cadena que consiste en N, E, S, W(para direcciones fijas) o *. Por lo tanto, puede buscar una solución general de tamaño 78 dando una cadena de 78 *s (entre comillas), o buscar una solución comenzando con el NEWSuso NEWSseguido de tantos *s como desee para pasos adicionales. Como primera prueba, tome su solución favorita y reemplace algunas de las letras *. Esto encuentra una solución rápida para un valor sorprendentemente alto de "algunos".

El programa le dirá qué laberinto agrega, descrito por la estructura de la pared y la posición de inicio, y también le dará la cantidad de posiciones y paredes alcanzables. Los laberintos se ordenan según estos criterios, y se agrega el primero sin resolver. Por lo tanto, la mayoría de los laberintos añadidos tienen (9/4), pero a veces otros también aparecen.

Tomé la solución conocida de longitud 79, y para cada grupo de 26 letras adyacentes, traté de reemplazarlas con 25 letras. También intenté eliminar 13 letras del principio y del final, y reemplazarlas por 13 al principio y 12 al final, y viceversa. Desafortunadamente, todo salió insatisfactorio. Entonces, ¿podemos tomar esto como un indicador de que la longitud 79 es óptima? No, igualmente intenté mejorar la solución de longitud 80 a longitud 79, y eso tampoco fue exitoso.

Finalmente, intenté combinar el comienzo de una solución con el final de la otra, y también con una solución transformada por una de las simetrías. Ahora me estoy quedando sin ideas interesantes, así que decidí mostrarte lo que tengo, a pesar de que no condujo a nuevas soluciones.

Christian Sievers
fuente
Esa fue una lectura realmente interesante. Tanto el nuevo enfoque como las diferentes formas de reducir la cantidad de laberintos para verificar. Para ser una respuesta válida, deberá incluir una cadena válida. No necesita ser una nueva cadena más corta, solo una cadena válida de cualquier longitud para dar una puntuación actual para este enfoque. Menciono esto porque sin una puntuación, la respuesta estará en riesgo de eliminación, y realmente me gustaría ver que se quede.
Trichoplax
¡También un buen trabajo para encontrar la solución de longitud óptima para el desafío relacionado más antiguo !
Trichoplax