String keluar labirin universal terpendek


48

Labirin pada kisi-kisi sel kotak N oleh N ditentukan dengan menentukan apakah setiap tepi adalah dinding atau bukan dinding. Semua tepi luar adalah dinding. Satu sel didefinisikan sebagai awal , dan satu sel didefinisikan sebagai pintu keluar , dan pintu keluar dapat dijangkau dari awal. Mulai dan keluar tidak pernah sel yang sama.

Perhatikan bahwa baik start maupun jalan keluar tidak perlu berada di perbatasan luar labirin, jadi ini adalah labirin yang valid:

Labirin 3 x 3 dengan jalan keluar di sel pusat

String 'N', 'E', 'S' dan 'W' menunjukkan upaya untuk memindahkan Utara, Timur, Selatan dan Barat masing-masing. Langkah yang diblokir oleh dinding dilewati tanpa gerakan. Sebuah string keluar dari maze jika menerapkan string itu dari awal menghasilkan keluar yang dicapai (terlepas dari apakah string berlanjut setelah mencapai keluar).

Terinspirasi oleh pertanyaan yang membingungkan. SE yang xnor menyediakan metode penyelesaian yang dapat dibuktikan dengan string yang sangat panjang, tulis kode yang dapat menemukan string tunggal yang keluar dari labirin 3 dengan 3.

Tidak termasuk labirin yang tidak valid (mulai dan keluar pada sel yang sama, atau keluar tidak dapat dijangkau dari awal) ada 138.172 labirin yang valid dan string harus keluar masing-masing.

Keabsahan

String harus memenuhi yang berikut ini:

  • Ini terdiri dari hanya karakter 'N', 'E', 'S' dan 'W'.
  • Itu keluar dari setiap labirin itu diterapkan, jika dimulai pada awal.

Karena himpunan semua labirin yang mungkin termasuk setiap labirin mungkin dengan setiap kemungkinan titik awal yang valid, ini secara otomatis berarti bahwa string akan tutup semua labirin dari setiap titik awal yang valid. Yaitu, dari titik awal mana titik keluar dapat dicapai.

Kemenangan

Pemenangnya adalah jawaban yang memberikan string valid terpendek dan termasuk kode yang digunakan untuk memproduksinya. Jika lebih dari satu jawaban memberikan string dengan panjang terpendek ini, yang pertama memposting panjang string yang menang.

Contoh

Berikut adalah contoh string 500 karakter yang panjang, untuk memberi Anda sesuatu untuk dikalahkan:

SEENSSNESSWNNSNNNNWWNWENENNWEENSESSNENSESWENWWWWWENWNWWSESNSWENNWNWENWSSSNNNNNNESWNEWWWWWNNNSWESSEEWNENWENEENNEEESEENSSEENNWWWNWSWNSSENNNWESSESNWESWEENNWSNWWEEWWESNWEEEWWSSSESEEWWNSSEEEEESSENWWNNSWNENSESSNEESENEWSSNWNSEWEEEWEESWSNNNEWNNWNWSSWEESSSSNESESNENNWEESNWEWSWNSNWNNWENSNSWEWSWWNNWNSENESSNENEWNSSWNNEWSESWENEEENSWWSNNNNSSNENEWSNEEWNWENEEWEESEWEEWSSESSSWNWNNSWNWENWNENWNSWESNWSNSSENENNNWSSENSSSWWNENWWWEWSEWSNSSWNNSEWEWENSWENWSENEENSWEWSEWWSESSWWWNWSSEWSNWSNNWESNSNENNSNEWSNNESNNENWNWNNNEWWEWEE

Terima kasih orlp untuk menyumbang ini.


Papan peringkat

Papan peringkat

Skor yang sama tercantum dalam urutan posting skor itu. Ini belum tentu urutan jawaban yang diposting karena skor untuk jawaban yang diberikan dapat diperbarui dari waktu ke waktu.


Hakim

Berikut ini adalah validator Python 3 yang mengambil string NESW sebagai argumen baris perintah atau melalui STDIN.

Untuk string yang tidak valid, ini akan memberi Anda contoh visual dari labirin yang gagal untuknya.


3
Ini pertanyaan yang sangat rapi. Apakah ada satu string terpendek (atau sejumlah string dan bukti bahwa tidak ada jawaban yang lebih pendek)? Dan jika demikian, tahukah Anda?
Alex Van Liew

1
@AlexReinking ya start bisa salah satu dari 9 sel dan jalan keluar bisa dari 9 sel, asalkan mereka bukan sel yang sama, dan jalan keluar dapat dijangkau dari awal.
trichoplax

1
Sedikit mirip dengan pertanyaan stackoverflow ini: stackoverflow.com/questions/26910401/… - tetapi sel awal dan akhir adalah kiri atas dan kanan bawah pada yang itu, yang mengurangi kemungkinan jumlah labirin menjadi 2423.
schnaader

1
@proudhaskeller bagaimanapun juga akan menjadi pertanyaan yang valid. Kasus umum, diberi skor untuk n = 3, akan membutuhkan kode yang lebih umum. Kasus khusus ini memungkinkan untuk optimisasi yang tidak berlaku untuk n umum, dan itulah cara saya memilih untuk menanyakannya.
trichoplax

2
Adakah yang menganggap mendekati masalah ini sebagai menemukan string yang paling pendek diterima untuk ekspresi reguler? Ini akan membutuhkan BANYAK pengurangan jumlah masalah sebelum mengkonversi ke regex, tetapi secara teoritis dapat menemukan solusi optimal yang dapat diverifikasi.
Kyle McCormick

Jawaban:


37

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

NNWSWNNSENESESWSWNSEENWWNWSSEWWNENWEENWSWNWSSENENWNWNESENESESWNWSESEWWNENWNEES

Strategi saya cukup sederhana - sebuah algoritma evolusi yang dapat menumbuhkan, mengecilkan, menukar elemen, dan mengubah urutan yang valid. Logika evolusi saya sekarang hampir sama dengan @ Sp3000, karena itu adalah peningkatan dari saya.

Namun, implementasi saya pada labirin logika agak bagus. Ini memungkinkan saya untuk memeriksa apakah string valid pada kecepatan blistering. Cobalah untuk mencari tahu dengan melihat komentar, do_movedan Mazekonstruktornya.

#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
Dikonfirmasi valid. Saya terkesan - saya tidak berharap melihat string sesingkat ini.
trichoplax

2
Saya akhirnya dapat menginstal gcc dan menjalankan ini untuk saya sendiri. Adalah hipnotis menyaksikan senar bermutasi dan perlahan menyusut ...
trichoplax

1
@trichoplax Saya katakan itu menyenangkan :)
orlp

2
@AlexReinking Saya memperbarui jawaban saya dengan kata implementasi. Jika Anda melihat pembongkaran, Anda akan melihatnya hanya selusin instruksi tanpa cabang atau memuat: coliru.stacked-crooked.com/a/3b09d36db85ce793 .
orlp

2
@AlexReinking Selesai. do_movesekarang sangat cepat.
orlp

16

Python 3 + PyPy, 82 80 karakter

SWWNNSENESESWSSWSEENWNWSWSEWNWNENENWWSESSEWSWNWSENWEENWWNNESENESSWNWSESESWWNNESE

Saya ragu-ragu untuk memposting jawaban ini karena saya pada dasarnya telah mengambil pendekatan orlp dan menempatkan putaran saya sendiri di atasnya. String ini ditemukan dengan memulai dengan solusi pseudorandom panjang 500 - cukup banyak benih yang dicoba sebelum saya dapat memecahkan rekor saat ini.

Satu-satunya optimasi utama baru adalah bahwa saya hanya melihat sepertiga dari labirin. Dua kategori labirin dikecualikan dari pencarian:

  • Labirin tempat <= 7kotak dapat dijangkau
  • Labirin tempat semua kotak yang dapat dijangkau berada di satu jalur, dan start / finish tidak di kedua ujungnya

Idenya adalah bahwa setiap string yang memecahkan sisa labirin juga harus menyelesaikan hal di atas secara otomatis. Saya yakin ini benar untuk tipe kedua, tetapi jelas tidak benar untuk tipe pertama, sehingga output akan mengandung beberapa false positive yang perlu diperiksa secara terpisah. Ini positif palsu biasanya hanya kehilangan sekitar 20 labirin, jadi saya pikir itu akan menjadi tradeoff yang baik antara kecepatan dan akurasi, dan itu juga akan memberi string sedikit lebih banyak ruang bernapas untuk bermutasi.

Awalnya saya membaca daftar panjang heuristik pencarian, tetapi secara mengerikan tidak ada satupun yang menghasilkan lebih dari 140 atau lebih.

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

Dikonfirmasi valid. Perbaikan yang bagus :)
trichoplax

Saya suka ide Anda untuk menyadari beberapa labirin tidak perlu diperiksa. Bisakah Anda entah bagaimana mengotomatiskan proses penentuan labirin mana yang cek berlebihan? Saya ingin tahu apakah itu akan menunjukkan lebih banyak labirin daripada labirin yang dapat dideduksi secara intuitif ...
trichoplax

Apa alasan Anda untuk tidak perlu memeriksa grafik jalur di mana permulaannya tidak di satu ujung? Kasus di mana finish tidak di satu ujung mudah dibenarkan, dan dapat diperkuat untuk tidak perlu memeriksa kasus di mana finish adalah titik potong, tapi saya tidak bisa melihat bagaimana membenarkan menghilangkan simpul start.
Peter Taylor

@PeterTaylor Setelah beberapa pemikiran, secara teori Anda benar, ada beberapa labirin yang tidak dapat Anda hilangkan seperti itu. Namun, sepertinya pada 3x3 tidak masalah untuk string selama ini.
orlp

2
@ orlp, Sp3000 membuat sketsa bukti dalam obrolan. Grafik jalur adalah kasus khusus. Nomor baru sel 0untuk nsepanjang jalan dan kira bahwa string Smembuat Anda dari 0ke n. Kemudian Sjuga membawa Anda dari sel perantara cke n. Misalkan sebaliknya. Membiarkan a(i)menjadi posisi setelah ilangkah-langkah mulai 0dan b(i)mulai c. Kemudian a(0) = 0 < b(0), setiap langkah berubah adan bpaling banyak 1, dan a(|S|) = n > b(|S|). Ambil yang terkecil tseperti itu a(t) >= b(t). Jelas a(t) != b(t)atau mereka akan sinkron, sehingga mereka harus bertukar tempat pada langkah tdengan bergerak ke arah yang sama.
Peter Taylor

3

C ++ dan perpustakaan dari lingeling

Ringkasan: Suatu pendekatan baru, tidak ada solusi baru , program yang bagus untuk dimainkan, dan beberapa hasil menarik dari ketidakberdayaan solusi lokal yang diketahui. Oh, dan beberapa pengamatan yang bermanfaat secara umum.

Dengan menggunakan pendekatan berbasis SAT , saya benar-benar bisa menyelesaikan masalah yang sama untuk labirin 4x4 dengan sel yang diblokir alih-alih dinding tipis dan tetap memulai dan keluar posisi di sudut yang berlawanan. Jadi saya berharap bisa menggunakan ide yang sama untuk masalah ini. Namun, meskipun untuk masalah lain saya hanya menggunakan 2.423 labirin (sementara itu telah mengamati bahwa 2083 sudah cukup) dan memiliki solusi panjang 29, pengkodean SAT menggunakan jutaan variabel dan menyelesaikannya butuh berhari-hari.

Jadi saya memutuskan untuk mengubah pendekatan dengan dua cara penting:

  • Jangan bersikeras mencari solusi dari awal, tetapi izinkan untuk memperbaiki bagian dari string solusi. (Lagipula itu mudah dilakukan dengan menambahkan klausa unit, tetapi program saya membuatnya nyaman untuk dilakukan.)
  • Jangan gunakan semua labirin dari awal. Alih-alih, tambahkan satu labirin secara bertahap secara bertahap. Beberapa labirin dapat diselesaikan secara kebetulan, atau mereka selalu dipecahkan ketika yang sudah dianggap diselesaikan. Dalam kasus terakhir, itu tidak akan pernah ditambahkan, tanpa kita perlu tahu implikasinya.

Saya juga melakukan beberapa optimasi untuk menggunakan lebih sedikit variabel dan klausa unit.

Program ini didasarkan pada @ orlp's. Perubahan penting adalah pemilihan labirin:

  • Pertama-tama, labirin diberikan oleh struktur dinding mereka dan posisi awal saja. (Mereka juga menyimpan posisi yang dapat dijangkau.) Fungsi ini is_solutionmemeriksa apakah semua posisi yang dapat dijangkau tercapai.
  • (Tidak berubah: masih tidak menggunakan labirin dengan hanya 4 atau kurang posisi yang dapat dijangkau. Tetapi sebagian besar dari mereka akan dibuang oleh pengamatan berikut.)
  • Jika labirin tidak menggunakan salah satu dari tiga sel teratas, ini setara dengan labirin yang digeser ke atas. Jadi kita bisa menjatuhkannya. Demikian juga untuk labirin yang tidak menggunakan salah satu dari tiga sel kiri.
  • Tidak masalah jika bagian yang tidak terjangkau dihubungkan, jadi kami bersikeras bahwa setiap sel yang tidak terjangkau sepenuhnya dikelilingi oleh dinding.
  • Sebuah labirin jalur tunggal yang merupakan sebuah labirin dari labirin jalur tunggal yang lebih besar selalu diselesaikan ketika yang lebih besar diselesaikan, jadi kita tidak membutuhkannya. Setiap labirin jalur tunggal dengan ukuran paling banyak 7 adalah bagian dari labirin yang lebih besar (masih pas dalam 3x3), tetapi ada labirin jalur 8 ukuran tunggal yang tidak. Untuk simpliciy, mari kita jatuhkan labirin jalur tunggal dengan ukuran kurang dari 8. (Dan saya masih menggunakan bahwa hanya titik ekstrim yang perlu dianggap sebagai posisi awal. Semua posisi digunakan sebagai posisi keluar, yang hanya penting untuk bagian SAT program.)

Dengan cara ini, saya mendapatkan total 1.0772 labirin dengan posisi awal.

Inilah programnya:

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

Pertama configure.shdan makeyang lingelingsolver, kemudian kompilasi program dengan sesuatu seperti g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl, di mana ...merupakan jalan mana lglib.hresp. liblgl.aadalah, jadi keduanya bisa jadi misalnya ../lingeling-<version>. Atau cukup taruh di direktori yang sama dan lakukan tanpa opsi -Idan -L.

Program ini memakan waktu satu wajib argumen baris perintah, string yang terdiri dari N, E, S, W(untuk arah tetap) atau *. Jadi Anda bisa mencari solusi umum ukuran 78 dengan memberikan string 78 *detik (dalam tanda kutip), atau mencari solusi yang dimulai dengan NEWSmenggunakan NEWSdiikuti oleh sebanyak yang *Anda inginkan untuk langkah-langkah tambahan. Sebagai tes pertama, ambil solusi favorit Anda dan ganti beberapa huruf dengan *. Ini menemukan solusi cepat untuk nilai "beberapa" yang sangat tinggi.

Program ini akan memberi tahu labirin mana yang ditambahkan, dijelaskan oleh struktur dinding dan posisi awal, dan juga memberikan jumlah posisi dan dinding yang dapat dijangkau. Labirin diurutkan berdasarkan kriteria ini, dan yang pertama tidak terpecahkan ditambahkan. Karena itu, kebanyakan labirin yang ditambahkan memiliki (9/4), tetapi terkadang labirin lain juga muncul.

Saya mengambil solusi yang diketahui panjang 79, dan untuk setiap kelompok 26 huruf yang berdekatan, mencoba untuk menggantinya dengan 25 huruf. Saya juga mencoba menghapus 13 huruf dari awal dan dari akhir, dan menggantinya dengan 13 di awal dan 12 di akhir, dan sebaliknya. Sayangnya, semua itu tidak memuaskan. Jadi, dapatkah kita menganggap ini sebagai indikator bahwa panjang 79 optimal? Tidak, saya juga mencoba meningkatkan solusi panjang ke panjang 79, dan itu juga tidak berhasil.

Akhirnya, saya mencoba menggabungkan awal dari satu solusi dengan ujung yang lain, dan juga dengan satu solusi yang ditransformasikan oleh salah satu simetri. Sekarang saya kehabisan ide menarik, jadi saya memutuskan untuk menunjukkan kepada Anda apa yang saya miliki, meskipun itu tidak mengarah pada solusi baru.


Itu bacaan yang sangat menarik. Baik pendekatan baru dan berbagai cara mengurangi jumlah labirin untuk diperiksa. Untuk menjadi jawaban yang valid, ini perlu menyertakan string yang valid. Tidak perlu menjadi string terpendek baru, string apa pun yang valid dengan panjang apa pun untuk memberikan skor saat ini untuk pendekatan ini. Saya menyebutkan ini karena tanpa skor, jawabannya akan beresiko dihapus, dan saya benar-benar ingin melihatnya tetap.
trichoplax

Juga kerja bagus untuk menemukan solusi panjang optimal untuk tantangan terkait yang lebih lama !
trichoplax
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.