Corda de saída de labirinto universal mais curta


48

Um labirinto em uma grade N por N de células quadradas é definido especificando se cada aresta é uma parede ou não. Todas as arestas externas são paredes. Uma célula é definida como o início , e uma célula é definida como a saída , e a saída é alcançável desde o início. O início e a saída nunca são a mesma célula.

Observe que nem o início nem a saída precisam estar na borda externa do labirinto, portanto, este é um labirinto válido:

Um labirinto 3 por 3 com a saída na célula central

Uma sequência de 'N', 'E', 'S' e 'W' indica a tentativa de mover para o norte, leste, sul e oeste, respectivamente. Um movimento bloqueado por uma parede é pulado sem movimento. Uma string sai de um labirinto se a aplicação dessa string desde o início resultar no alcance da saída (independentemente de a string continuar depois de chegar à saída).

Inspirada por essa pergunta intrigante.SE para a qual o xnor forneceu um método comprovável de solução de uma string muito longa, escreva um código que possa encontrar uma única string que saia de qualquer labirinto de 3 por 3.

Excluindo labirintos inválidos (iniciar e sair na mesma célula ou saída inacessível desde o início), existem 138.172 labirintos válidos e a string deve sair de cada um deles.

Validade

A cadeia deve satisfazer o seguinte:

  • É composto apenas pelos caracteres 'N', 'E', 'S' e 'W'.
  • Ele sai de qualquer labirinto ao qual é aplicado, se iniciado no início.

Como o conjunto de todos os labirintos possíveis inclui cada labirinto possível com cada ponto de partida válido possível, isso significa automaticamente que a string sairá de qualquer labirinto de qualquer ponto de partida válido. Ou seja, a partir de qualquer ponto de partida a partir do qual a saída seja alcançável.

Ganhando

O vencedor é a resposta que fornece a menor seqüência válida e inclui o código usado para produzi-la. Se mais de uma das respostas fornecer uma string com o menor comprimento, a primeira a postar esse comprimento vence.

Exemplo

Aqui está um exemplo de 500 caracteres, para oferecer a você algo para vencer:

SEENSSNESSWNNSNNNNWWNWENENNWEENSESSNENSESWENWWWWWENWNWWSESNSWENNWNWENWSSSNNNNNNESWNEWWWWWNNNSWESSEEWNENWENEENNEEESEENSSEENNWWWNWSWNSSENNNWESSESNWESWEENNWSNWWEEWWESNWEEEWWSSSESEEWWNSSEEEEESSENWWNNSWNENSESSNEESENEWSSNWNSEWEEEWEESWSNNNEWNNWNWSSWEESSSSNESESNENNWEESNWEWSWNSNWNNWENSNSWEWSWWNNWNSENESSNENEWNSSWNNEWSESWENEEENSWWSNNNNSSNENEWSNEEWNWENEEWEESEWEEWSSESSSWNWNNSWNWENWNENWNSWESNWSNSSENENNNWSSENSSSWWNENWWWEWSEWSNSSWNNSEWEWENSWENWSENEENSWEWSEWWSESSWWWNWSSEWSNWSNNWESNSNENNSNEWSNNESNNENWNWNNNEWWEWEE

Obrigado ao orlp por doar isso.


Entre os melhores

Entre os melhores

As pontuações iguais são listadas em ordem de publicação dessa pontuação. Essa não é necessariamente a ordem em que as respostas foram postadas, pois a pontuação de uma determinada resposta pode ser atualizada ao longo do tempo.


Juiz

Aqui está um validador Python 3 que utiliza uma sequência de caracteres NESW como argumento de linha de comando ou via STDIN.

Para uma sequência inválida, isso fornecerá um exemplo visual de um labirinto em que ela falha.


3
Esta é uma pergunta realmente interessante. Existe uma string mais curta (ou um número de strings e uma prova de que não pode haver respostas mais curtas)? E se sim, você sabe disso?
Alex Van Liew

11
@AlexReinking sim, o início pode ser qualquer uma das 9 células e a saída pode ser qualquer uma das 9 células, desde que não sejam a mesma célula e a saída é alcançável desde o início.
Trichoplax

11
Ligeiramente semelhante a esta pergunta stackoverflow: stackoverflow.com/questions/26910401/... - mas iniciar e celular final é superior esquerdo e inferior direito, em que um, o que reduz a possível contagem labirinto para 2423.
schnaader

11
@proudhaskeller de qualquer forma seria uma pergunta válida. O caso geral, pontuado para n = 3, exigiria código mais generalizado. Esse caso específico permite otimizações que não se aplicam ao n geral, e foi assim que escolhi perguntar.
Trichoplax

2
Alguém considerou abordar esse problema como encontrar a menor string aceita para uma expressão regular? Exigiria MUITAS reduções no número de problemas antes da conversão para regexes, mas teoricamente poderia encontrar uma solução ideal verificável.
Kyle McCormick

Respostas:


37

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

NNWSWNNSENESESWSSWNSEENWWNWSSEWWNENWEENWSWNWSSENENWNWNESENESESWNWSESEWWNENWNEES

Minha estratégia é bastante simples - um algoritmo de evolução que pode crescer, encolher, trocar elementos e alterar seqüências válidas. Minha lógica de evolução agora é quase a mesma da @ Sp3000, pois a dele foi uma melhoria em relação à minha.

No entanto, minha implementação da lógica do labirinto é bastante bacana. Isso me permite verificar se as strings são válidas na velocidade de formação de bolhas. Tente descobrir olhando o comentário do_movee o Mazeconstrutor.

#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
Confirmado válido. Estou impressionado - não esperava ver cordas tão curtas.
Trichoplax

2
Finalmente comecei a instalar o gcc e a executar isso sozinho. É hipnótico assistindo as cordas mutação e encolhendo lentamente ...
Trichoplax

11
@trichoplax Eu lhe disse que foi divertido :)
orlp

2
@AlexReinking Atualizei minha resposta com a referida implementação. Se você observar a desmontagem, verá apenas uma dúzia de instruções sem ramificação ou carga: coliru.stacked-crooked.com/a/3b09d36db85ce793 .
orlp 4/08/15

2
@AlexReinking Done. do_moveagora é incrivelmente rápido.
orlp 4/08/15

16

Python 3 + PyPy, 82 80 caracteres

SWWNNSENESESWSSWSEENWNWSWSEWNWNENENWWSESSEWSWNWSENWEENWWNNESENESSWNWSESESWWNNESE

Eu tenho hesitado em postar esta resposta, porque eu basicamente peguei a abordagem do orlp e coloquei minha própria rotação nela. Essa string foi encontrada iniciando com uma solução de tamanho pseudo-aleatório 500 - várias sementes foram testadas antes que eu pudesse quebrar o recorde atual.

A única nova grande otimização é que eu olho apenas um terço dos labirintos. Duas categorias de labirintos são excluídas da pesquisa:

  • Labirintos onde os <= 7quadrados são acessíveis
  • Labirintos em que todos os quadrados alcançáveis ​​estão em um único caminho e o início / fim não está nas duas extremidades

A idéia é que qualquer corda que resolva o resto dos labirintos também resolva o problema acima automaticamente. Estou convencido de que isso é verdade para o segundo tipo, mas definitivamente não é verdade para o primeiro, portanto a saída conterá alguns falsos positivos que precisam ser verificados separadamente. No entanto, esses falsos positivos geralmente perdem apenas cerca de 20 labirintos, então eu pensei que seria uma boa troca entre velocidade e precisão, e também daria às cordas um pouco mais de espaço para respirar.

Inicialmente, examinei uma longa lista de heurísticas de pesquisa, mas horrorosamente nenhuma delas apresentou algo melhor que 140 ou mais.

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

Confirmado válido. Melhorias agradáveis :)
Trichoplax

Eu gosto da sua ideia de perceber que alguns labirintos não precisam ser verificados. Você poderia de alguma forma automatizar o processo de determinar quais labirintos são verificações redundantes? Estou curioso para saber se isso iria aparecer mais labirintos do que os que podem ser deduzidas intuitivamente ...
Trichoplax

Qual é o seu motivo para não precisar verificar os gráficos de caminho onde o início não está em uma extremidade? O caso em que o acabamento não está em uma extremidade é fácil de justificar e pode ser reforçado por não ser necessário verificar casos em que o acabamento é um vértice cortado, mas não consigo ver como justificar a eliminação dos vértices de início.
Peter Taylor

@ PeterTaylor Depois de pensar um pouco mais, teoricamente você está certo, existem alguns labirintos que você não pode eliminar assim. No entanto, parece que em 3x3 não importa para seqüências de caracteres por tanto tempo.
orlp 9/08/2015

2
@orlp, o Sp3000 esboçou uma prova no chat. Os gráficos de caminho são um caso especial. Renumerar as células 0para nao longo do caminho e suponha que a seqüência Sleva você de 0para n. Em seguida, Svocê também de qualquer célula intermediária cpara n. Suponha o contrário. Let a(i)Ser a posição após as ietapas começando em 0e b(i)começando em c. Então a(0) = 0 < b(0), cada etapa muda ae b, no máximo, 1 e a(|S|) = n > b(|S|). Pegue o menor ttal que a(t) >= b(t). Claramente a(t) != b(t)ou eles estariam sincronizados, eles devem trocar de lugar na etapa t, movendo-se na mesma direção.
Peter Taylor

3

C ++ e a biblioteca do lingeling

Resumo: Uma nova abordagem, sem novas soluções , um bom programa para brincar e alguns resultados interessantes da não-improvabilidade local das soluções conhecidas. Ah, e algumas observações geralmente úteis.

Usando uma abordagem baseada em SAT , eu poderia resolver completamente o problema semelhante para labirintos 4x4 com células bloqueadas em vez de paredes finas e posições fixas de partida e saída em cantos opostos. Então, eu esperava poder usar as mesmas idéias para esse problema. No entanto, mesmo para o outro problema, eu usei apenas 2423 labirintos (enquanto isso foi observado em 2083) e ele tem uma solução de comprimento 29, a codificação SAT usou milhões de variáveis ​​e a solução levou dias.

Então, decidi mudar a abordagem de duas maneiras importantes:

  • Não insista em procurar uma solução a partir do zero, mas permita corrigir uma parte da cadeia de solução. (É fácil fazer isso adicionando cláusulas de unidade, mas meu programa facilita a execução.)
  • Não use todos os labirintos desde o início. Em vez disso, adicione gradualmente um labirinto não resolvido de cada vez. Alguns labirintos podem ser resolvidos por acaso, ou sempre são resolvidos quando os já considerados são resolvidos. No último caso, ele nunca será adicionado, sem precisarmos saber a implicação.

Também fiz algumas otimizações para usar menos variáveis ​​e cláusulas de unidade.

O programa é baseado em @ orlp's. Uma mudança importante foi a seleção de labirintos:

  • Em primeiro lugar, os labirintos são dados apenas pela estrutura da parede e pela posição inicial. (Eles também armazenam as posições alcançáveis.) A função is_solutionverifica se todas as posições alcançáveis ​​são alcançadas.
  • (Inalterado: ainda não usando labirintos com apenas 4 ou menos posições alcançáveis. Mas a maioria deles seria jogada fora de qualquer maneira pelas seguintes observações.)
  • Se um labirinto não usa nenhuma das três células superiores, é equivalente a um labirinto que é deslocado para cima. Para que possamos abandoná-lo. Da mesma forma para um labirinto que não usa nenhuma das três células esquerdas.
  • Não importa se partes inacessíveis estão conectadas, por isso insistimos que cada célula inacessível esteja completamente cercada por paredes.
  • Um labirinto de caminho único que é uma sub-imagem de um labirinto de caminho único maior sempre é resolvido quando o maior é resolvido, portanto não precisamos dele. Cada labirinto de tamanho único no máximo 7 faz parte de um maior (ainda em tamanho 3x3), mas existem labirintos de tamanho único de tamanho 8 que não são. Por uma questão de simplicidade, vamos soltar labirintos de caminho único de tamanho menor que 8. (E ainda estou usando que apenas os pontos extremos precisam ser considerados como posições iniciais. Todas as posições são usadas como posições de saída, o que é importante apenas para a parte SAT do programa.)

Dessa forma, recebo um total de 10772 labirintos com posições iniciais.

Aqui está o 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);
  }
}  

Primeiro configure.she makeo lingelingsolucionador, compile o programa com algo como g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl, onde ...está o caminho em que lglib.hresp. liblgl.asão, então ambos poderiam ser, por exemplo ../lingeling-<version>. Ou simplesmente coloque-os no mesmo diretório e sem as opções -Ie -L.

O programa tem um argumento de linha de comando obrigatório, uma cadeia que consiste N, E, S, W(para direções fixas) ou *. Portanto, você pode procurar uma solução geral de tamanho 78, fornecendo uma sequência de 78 *s (entre aspas) ou procurar uma solução começando com NEWS, NEWSseguido de tantos *s quanto desejar para etapas adicionais. Como primeiro teste, pegue sua solução favorita e substitua algumas das letras por *. Isso encontra uma solução rápida para um valor surpreendentemente alto de "alguns".

O programa dirá qual labirinto ele adiciona, descrito pela estrutura da parede e posição inicial, e também fornecerá o número de posições e paredes alcançáveis. Os labirintos são classificados por esses critérios e o primeiro não resolvido é adicionado. Portanto, a maioria dos labirintos adicionados tem (9/4), mas às vezes outros aparecem também.

Peguei a solução conhecida de comprimento 79 e, para cada grupo de 26 letras adjacentes, tentei substituí-las por 25. Também tentei remover 13 letras do começo e do fim e substituí-las por 13 no início e 12 no final e vice-versa. Infelizmente, tudo saiu insatisfatório. Então, podemos tomar isso como um indicador de que o comprimento 79 é ideal? Não, tentei da mesma forma melhorar a solução do comprimento 80 para o comprimento 79, e isso também não teve êxito.

Por fim, tentei combinar o início de uma solução com o final da outra e também com uma solução transformada por uma das simetrias. Agora que estou ficando sem idéias interessantes, decidi mostrar o que tenho, mesmo que não tenha levado a novas soluções.


Essa foi uma leitura realmente interessante. Tanto a nova abordagem quanto as diferentes maneiras de reduzir o número de labirintos a serem verificados. Para ser uma resposta válida, será necessário incluir uma sequência válida. Não precisa ser uma nova sequência mais curta, apenas uma sequência válida de qualquer tamanho para fornecer uma pontuação atual para essa abordagem. Menciono isso porque, sem uma pontuação, a resposta estará em risco de exclusão e eu realmente gostaria que ela permanecesse.
trichoplax

Também foi um bom trabalho encontrar a solução ideal para o desafio mais antigo !
trichoplax
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.