Chuỗi thoát mê cung phổ quát ngắn nhất


48

Một mê cung trên lưới N by N của các ô vuông được xác định bằng cách chỉ định xem mỗi cạnh có phải là tường hay không. Tất cả các cạnh bên ngoài là bức tường. Một ô được định nghĩa là bắt đầu và một ô được xác định là lối ra và lối ra có thể truy cập được từ đầu. Bắt đầu và thoát không bao giờ là cùng một ô.

Lưu ý rằng không phải lối ra cũng không phải lối ra ở biên ngoài của mê cung, vì vậy đây là mê cung hợp lệ:

Một mê cung 3 trong 3 với lối ra trên ô trung tâm

Một chuỗi 'N', 'E', 'S' và 'W' biểu thị nỗ lực di chuyển theo hướng Bắc, Đông, Nam và Tây tương ứng. Một di chuyển bị chặn bởi một bức tường được bỏ qua mà không di chuyển. Một chuỗi thoát ra một mê cung nếu áp dụng chuỗi đó từ bắt đầu dẫn đến việc thoát được tiếp cận (bất kể chuỗi đó có tiếp tục sau khi đạt được thoát không).

Lấy cảm hứng từ câu hỏi khó hiểu này. Câu hỏixnor đã cung cấp một phương pháp giảithể chứng minh được bằng một chuỗi rất dài, viết mã có thể tìm thấy một chuỗi duy nhất thoát khỏi bất kỳ 3 mê cung nào.

Không bao gồm các mê cung không hợp lệ (bắt đầu và thoát trên cùng một ô hoặc thoát không thể truy cập từ đầu), có 138.172 mê cung hợp lệ và chuỗi phải thoát khỏi từng ô đó.

Hiệu lực

Chuỗi phải đáp ứng các điều sau:

  • Nó chỉ bao gồm các ký tự 'N', 'E', 'S' và 'W'.
  • Nó thoát khỏi bất kỳ mê cung nào nó được áp dụng, nếu bắt đầu khi bắt đầu.

Vì tập hợp tất cả các mê cung có thể bao gồm mỗi mê cung có thể có với mỗi điểm bắt đầu hợp lệ có thể, điều này tự động có nghĩa là chuỗi sẽ thoát khỏi bất kỳ mê cung nào từ bất kỳ điểm bắt đầu hợp lệ nào. Đó là, từ bất kỳ điểm bắt đầu mà lối ra có thể truy cập.

Chiến thắng

Người chiến thắng là câu trả lời cung cấp chuỗi hợp lệ ngắn nhất và bao gồm mã được sử dụng để sản xuất chuỗi đó. Nếu có nhiều câu trả lời cung cấp một chuỗi có độ dài ngắn nhất này, thì câu đầu tiên đăng độ dài chuỗi đó sẽ thắng.

Thí dụ

Dưới đây là một chuỗi ví dụ dài 500 ký tự, để cung cấp cho bạn thứ gì đó để đánh bại:

SEENSSNESSWNNSNNNNWWNWENENNWEENSESSNENSESWENWWWWWENWNWWSESNSWENNWNWENWSSSNNNNNNESWNEWWWWWNNNSWESSEEWNENWENEENNEEESEENSSEENNWWWNWSWNSSENNNWESSESNWESWEENNWSNWWEEWWESNWEEEWWSSSESEEWWNSSEEEEESSENWWNNSWNENSESSNEESENEWSSNWNSEWEEEWEESWSNNNEWNNWNWSSWEESSSSNESESNENNWEESNWEWSWNSNWNNWENSNSWEWSWWNNWNSENESSNENEWNSSWNNEWSESWENEEENSWWSNNNNSSNENEWSNEEWNWENEEWEESEWEEWSSESSSWNWNNSWNWENWNENWNSWESNWSNSSENENNNWSSENSSSWWNENWWWEWSEWSNSSWNNSEWEWENSWENWSENEENSWEWSEWWSESSWWWNWSSEWSNWSNNWESNSNENNSNEWSNNESNNENWNWNNNEWWEWEE

Cảm ơn orlp đã đóng góp này.


Bảng xếp hạng

Bảng xếp hạng

Điểm bằng nhau được liệt kê theo thứ tự đăng điểm đó. Đây không nhất thiết là thứ tự mà các câu trả lời đã được đăng vì điểm cho một câu trả lời nhất định có thể được cập nhật theo thời gian.


Thẩm phán

Dưới đây là trình xác nhận Python 3 lấy một chuỗi NESW làm đối số dòng lệnh hoặc thông qua STDIN.

Đối với một chuỗi không hợp lệ, điều này sẽ cung cấp cho bạn một ví dụ trực quan về một mê cung mà nó thất bại.


3
Đây là một câu hỏi thực sự gọn gàng. Có một chuỗi ngắn nhất (hoặc một số chuỗi và bằng chứng rằng không thể có câu trả lời ngắn hơn) không? Và nếu vậy, bạn có biết nó?
Alex Van Liew

1
@AlexReinking có, bắt đầu có thể là một trong số 9 ô và lối ra có thể là bất kỳ ô nào trong số 9 ô, miễn là chúng không phải là cùng một ô và có thể truy cập được từ đầu.
trichoplax

1
Khá giống với câu hỏi stackoverflow này: stackoverflow.com/questions/26910401/ Khăn - nhưng ô bắt đầu và kết thúc nằm trên cùng bên trái và dưới cùng bên phải trong đó, làm giảm số lượng mê cung có thể xuống còn 2423.
schnaader

1
@proudhaskeller một trong hai cách sẽ là một câu hỏi hợp lệ. Trường hợp chung, được tính cho n = 3, sẽ yêu cầu mã tổng quát hơn. Trường hợp cụ thể này cho phép tối ưu hóa không áp dụng cho n chung và đó là cách tôi chọn để hỏi nó.
trichoplax

2
Có ai coi việc tiếp cận vấn đề này là tìm chuỗi ngắn nhất được chấp nhận cho một biểu thức chính quy không? Nó sẽ yêu cầu RẤT NHIỀU việc giảm số lượng vấn đề trước khi chuyển đổi sang biểu thức chính quy, nhưng về mặt lý thuyết có thể tìm ra giải pháp tối ưu có thể kiểm chứng.
Kyle McCormick

Câu trả lời:


37

C ++, 97 95 93 91 86 83 82 81 79 ký tự

NNWSWNNSENESESWSSWNSEENWWNWSSEWWNENCHENWWWWWSSENENWNWNESENESESWNWSESEWWNENWNEES

Chiến lược của tôi khá đơn giản - một thuật toán tiến hóa có thể phát triển, thu nhỏ, hoán đổi các yếu tố và thay đổi các chuỗi hợp lệ. Logic tiến hóa của tôi bây giờ gần giống với @ Sp3000, vì nó là một cải tiến so với của tôi.

Tuy nhiên, việc thực hiện logic mê cung của tôi khá tiện lợi. Điều này cho phép tôi kiểm tra xem các chuỗi có hợp lệ ở tốc độ phồng hay không. Cố gắng tìm ra nó bằng cách nhìn vào bình luận do_movevà hàm Mazetạo.

#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
Khẳng định hợp lệ. Tôi rất ấn tượng - Tôi không mong đợi sẽ thấy chuỗi này ngắn.
trichoplax

2
Cuối cùng tôi cũng có vòng để cài đặt gcc và tự chạy nó. Thật là thôi miên khi nhìn các chuỗi đột biến và dần dần co lại ...
trichoplax

1
@trichoplax Tôi nói với bạn rằng nó rất vui :)
orlp 29/07/2015

2
@AlexReinking Tôi đã cập nhật câu trả lời của mình với việc thực hiện. Nếu bạn nhìn vào quá trình tháo gỡ, bạn sẽ thấy đó chỉ là một tá hướng dẫn mà không có bất kỳ chi nhánh hay tải nào: coliru.stacked-crooking.com/a/3b09d36db85ce793 .
orlp

2
@AlexReinking Xong. do_movebây giờ cực kỳ nhanh
orlp

16

Python 3 + PyPy, 82 80 ký tự

SWWNNSENESESWSSWSEENWNWSWSEWNWNENENWWSESSEWSWNWSENWEENWWNNESENESSWNWSESESWWNNESE

Tôi đã do dự để đăng câu trả lời này vì về cơ bản tôi đã thực hiện phương pháp của orlp và đặt guồng quay của riêng mình lên nó. Chuỗi này đã được tìm thấy bằng cách bắt đầu với một giải pháp giả ngẫu nhiên có độ dài 500 - khá nhiều hạt giống đã được thử trước khi tôi có thể phá vỡ kỷ lục hiện tại.

Tối ưu hóa mới duy nhất là tôi chỉ nhìn vào một phần ba mê cung. Hai loại mê cung được loại trừ khỏi tìm kiếm:

  • Mazes nơi <= 7hình vuông có thể truy cập
  • Mazes nơi tất cả các ô vuông có thể tiếp cận nằm trên một đường và điểm bắt đầu / kết thúc không ở cả hai đầu

Ý tưởng là bất kỳ chuỗi nào giải quyết phần còn lại của mê cung cũng sẽ tự động giải quyết các vấn đề trên. Tôi tin rằng điều này đúng với loại thứ hai, nhưng nó chắc chắn không đúng với loại thứ nhất, vì vậy đầu ra sẽ chứa một số dương tính giả cần được kiểm tra riêng. Những dương tính giả này thường chỉ bỏ lỡ khoảng 20 mê cung, vì vậy tôi nghĩ rằng đó là một sự đánh đổi tốt giữa tốc độ và độ chính xác, và nó cũng sẽ tạo cho dây có thêm một chút không gian thở để biến đổi.

Ban đầu tôi đã trải qua một danh sách dài các heuristic tìm kiếm, nhưng thật kinh khủng không ai trong số họ đưa ra bất cứ điều gì tốt hơn 140 hoặc hơn thế.

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

Khẳng định hợp lệ. Những cải tiến tốt đẹp :)
trichoplax

Tôi thích ý tưởng của bạn về việc nhận ra một số mê cung không cần phải kiểm tra. Bạn có thể bằng cách nào đó tự động hóa quá trình xác định mê cung nào là kiểm tra dư thừa? Tôi tò mò muốn biết liệu điều đó có xuất hiện nhiều mê cung hơn những thứ có thể suy ra bằng trực giác không ...
trichoplax

Lý do nào khiến bạn không cần kiểm tra biểu đồ đường dẫn trong đó điểm bắt đầu không ở một đầu? Trường hợp kết thúc không ở một đầu là dễ dàng để biện minh và có thể được củng cố để không cần kiểm tra các trường hợp trong đó kết thúc là một đỉnh bị cắt, nhưng tôi không thể xem làm thế nào để loại bỏ các đỉnh bắt đầu.
Peter Taylor

@PeterTaylor Sau khi suy nghĩ thêm, về mặt lý thuyết bạn đã đúng, có một số mê cung mà bạn không thể loại bỏ như thế. Tuy nhiên, dường như trên 3x3, chuỗi này không thành vấn đề.
orlp

2
@orlp, Sp3000 đã phác thảo một bằng chứng trong trò chuyện. Biểu đồ đường dẫn là một trường hợp đặc biệt. Renumber các tế bào 0để ndọc theo con đường và giả sử rằng chuỗi Sđược bạn từ 0đến n. Sau đó, Scũng sẽ đưa bạn từ bất kỳ tế bào trung gian cđến n. Giả sử khác đi. Hãy a(i)là vị trí sau icác bước bắt đầu 0b(i)bắt đầu từ c. Sau đó a(0) = 0 < b(0), mỗi bước thay đổi abnhiều nhất là 1, và a(|S|) = n > b(|S|). Lấy cái nhỏ nhất tsao cho a(t) >= b(t). Rõ ràng a(t) != b(t)hoặc họ sẽ đồng bộ, vì vậy họ phải trao đổi địa điểm từng bước tbằng cách di chuyển theo cùng một hướng.
Peter Taylor

3

C ++ và thư viện từ lingeling

Tóm tắt: Một cách tiếp cận mới, không có giải pháp mới , một chương trình hay để chơi và một số kết quả thú vị về tính không ngẫu hứng cục bộ của các giải pháp đã biết. Oh, và một số quan sát thường hữu ích.

Sử dụng phương pháp tiếp cận dựa trên SAT , tôi hoàn toàn có thể giải quyết vấn đề tương tự đối với các mê cung 4 x 4 với các ô bị chặn thay vì các bức tường mỏng và các vị trí bắt đầu và thoát cố định ở các góc đối diện. Vì vậy, tôi hy vọng có thể sử dụng những ý tưởng tương tự cho vấn đề này. Tuy nhiên, mặc dù đối với vấn đề khác, tôi chỉ sử dụng 2423 mê cung (trong khi đó, theo quan sát thì năm 2083 là đủ) và nó có một giải pháp dài 29, mã hóa SAT đã sử dụng hàng triệu biến số và giải quyết được mất nhiều ngày.

Vì vậy, tôi quyết định thay đổi cách tiếp cận theo hai cách quan trọng:

  • Đừng khăng khăng tìm kiếm giải pháp từ đầu, nhưng cho phép sửa một phần của chuỗi giải pháp. (Điều đó dễ thực hiện bằng cách thêm các mệnh đề đơn vị, nhưng chương trình của tôi làm cho nó thoải mái để làm.)
  • Đừng sử dụng tất cả các mê cung ngay từ đầu. Thay vào đó, tăng dần một mê cung chưa giải quyết tại một thời điểm. Một số mê cung có thể được giải quyết một cách tình cờ, hoặc chúng luôn được giải quyết khi những cái đã được xem xét được giải quyết. Trong trường hợp sau, nó sẽ không bao giờ được thêm vào, mà chúng ta không cần phải biết hàm ý.

Tôi cũng đã thực hiện một số tối ưu hóa để sử dụng ít biến và mệnh đề đơn vị.

Chương trình này dựa trên @ orlp's. Một thay đổi quan trọng là lựa chọn mê cung:

  • Trước hết, mê cung được cho bởi cấu trúc tường và vị trí bắt đầu. (Họ cũng lưu trữ các vị trí có thể tiếp cận.) Chức năng is_solutionkiểm tra nếu đạt được tất cả các vị trí có thể tiếp cận.
  • (Không thay đổi: vẫn không sử dụng mê cung chỉ có 4 vị trí có thể tiếp cận hoặc ít hơn. Nhưng hầu hết trong số chúng sẽ bị vứt đi dù sao đi nữa bởi các quan sát sau đây.)
  • Nếu một mê cung không sử dụng bất kỳ một trong ba ô trên cùng, thì nó tương đương với một mê cung được dịch chuyển lên. Vì vậy, chúng tôi có thể thả nó. Tương tự như vậy đối với một mê cung không sử dụng bất kỳ một trong ba ô bên trái.
  • Sẽ không có vấn đề gì nếu các phần không thể truy cập được kết nối, vì vậy chúng tôi khẳng định rằng mỗi ô không thể truy cập được bao quanh hoàn toàn bởi các bức tường.
  • Một mê cung đường dẫn duy nhất là một mê cung của một mê cung đường dẫn lớn hơn luôn luôn được giải quyết khi một mê cung đường lớn hơn được giải quyết, vì vậy chúng ta không cần nó. Mỗi mê cung đường dẫn có kích thước tối đa 7 là một phần của một mê cung lớn hơn (vẫn phù hợp với 3x3), nhưng có những mê cung đường dẫn đơn kích thước 8 không có. Để đơn giản, chúng ta chỉ cần thả các mê cung đường dẫn có kích thước nhỏ hơn 8. (Và tôi vẫn đang sử dụng chỉ các điểm cực trị cần được coi là vị trí bắt đầu. Tất cả các vị trí được sử dụng làm vị trí thoát, chỉ quan trọng đối với phần SAT của chương trình.)

Bằng cách này, tôi nhận được tổng cộng 10772 mê cung với vị trí bắt đầu.

Đây là chương trình:

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

Đầu tiên configure.shmakengười lingelinggiải quyết, sau đó biên dịch chương trình với một cái gì đó như g++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl, đâu ...là con đường nơi tôn trọng lglib.h. liblgl.alà, vì vậy cả hai có thể là ví dụ ../lingeling-<version>. Hoặc chỉ cần đặt chúng trong cùng một thư mục và làm mà không có -I-Lcác tùy chọn.

Chương trình này mất một đối số dòng lệnh bắt buộc, một chuỗi bao gồm N, E, S, W(đối với hướng cố định) hoặc *. Vì vậy, bạn có thể tìm kiếm một giải pháp chung có kích thước 78 bằng cách đưa ra một chuỗi 78 *giây (trong ngoặc kép) hoặc tìm kiếm một giải pháp bắt đầu NEWSbằng cách sử dụng NEWStheo sau là nhiều *s như bạn muốn cho các bước bổ sung. Như một thử nghiệm đầu tiên, lấy giải pháp yêu thích của bạn và thay thế một số chữ cái bằng *. Điều này tìm thấy một giải pháp nhanh chóng cho giá trị cao của "một số" đáng ngạc nhiên.

Chương trình sẽ cho biết nó thêm mê cung nào, được mô tả bởi cấu trúc tường và vị trí bắt đầu, đồng thời cũng đưa ra số lượng vị trí và tường có thể tiếp cận. Các mê cung được sắp xếp theo các tiêu chí này, và tiêu chí chưa được giải quyết đầu tiên được thêm vào. Do đó, hầu hết các mê cung được thêm vào (9/4), nhưng đôi khi những người khác cũng xuất hiện.

Tôi đã lấy giải pháp đã biết có độ dài 79, và với mỗi nhóm 26 chữ cái liền kề, đã cố gắng thay thế chúng bằng 25 chữ cái bất kỳ. Tôi cũng đã cố gắng loại bỏ 13 chữ cái từ đầu và cuối, và thay thế chúng bằng 13 chữ cái ở đầu và 12 chữ cái ở cuối và ngược lại. Thật không may, tất cả đã đi ra không thỏa mãn. Vì vậy, chúng ta có thể coi đây là chỉ số cho thấy chiều dài 79 là tối ưu? Không, tôi cũng đã cố gắng cải thiện giải pháp chiều dài 80 thành chiều dài 79 và điều đó cũng không thành công.

Cuối cùng, tôi đã thử kết hợp sự bắt đầu của một giải pháp với kết thúc của giải pháp kia và cũng với một giải pháp được biến đổi bởi một trong các đối xứng. Bây giờ tôi đang cạn kiệt những ý tưởng thú vị, vì vậy tôi quyết định cho bạn thấy những gì tôi có, mặc dù nó không dẫn đến những giải pháp mới.


Đó là một bài đọc thực sự thú vị. Cả cách tiếp cận mới và cách thức khác nhau để giảm số lượng mê cung cần kiểm tra. Để là một câu trả lời hợp lệ, điều này sẽ cần bao gồm một chuỗi hợp lệ. Nó không cần phải là một chuỗi ngắn nhất mới, chỉ là bất kỳ chuỗi hợp lệ có độ dài bất kỳ để đưa ra điểm số hiện tại cho phương pháp này. Tôi đề cập đến điều này bởi vì không có điểm số, câu trả lời sẽ có nguy cơ bị xóa và tôi thực sự muốn thấy nó ở lại.
trichoplax

Cũng rất tốt trong việc tìm kiếm giải pháp chiều dài tối ưu cho các thách thức liên quan cũ !
trichoplax
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.