Найкоротший універсальний рядок виходу з лабіринту


48

Лабіринт на сітці N від N квадратних комірок визначається, вказавши, чи кожен край є стіною чи ні стіною. Всі зовнішні краї - стіни. Одна комірка визначається як старт , а одна комірка визначається як вихід , а вихід доступний із початку. Початок і вихід ніколи не є одна і та ж комірка.

Зауважте, що ні старт, ні вихід не повинні бути на зовнішній межі лабіринту, тому це дійсний лабіринт:

Лабіринт 3 на 3 з виходом на центральну клітинку

Рядок "N", "E", "S" і "W" вказує на спробу переміщення відповідно на Північ, Схід, Південь та Захід. Рух, який перегороджений стіною, пропускається без руху. Рядок виходить з лабіринту, якщо застосувати цей рядок із початкових результатів до досягнення досягнутого виходу (незалежно від того, чи продовжує рядок після досягнення виходу).

Натхненні цим puzzling.SE питання , для якого XNOR запропонований доказовий метод вирішення з дуже довгою рядком, код записи , який може знайти один рядок , яка виходить з будь-яких 3 на 3 лабіринт.

За винятком недійсних лабіринтів (запуск і вихід з однієї комірки або вихід із недоступного запуску) існує 138 172 дійсних лабіринтів і рядок повинен вийти з кожного з них.

Дійсність

Рядок повинен відповідати наступному:

  • Він складається лише з символів "N", "E", "S" і "W".
  • Він закриває будь-який лабіринт, до якого він застосований, якщо його запустити на старті.

Оскільки набір усіх можливих лабіринтів включає кожен можливий лабіринт з кожною можливою дійсною початковою точкою, це автоматично означає, що рядок вийде з будь-якого лабіринту з будь-якої дійсної початкової точки. Тобто з будь-якої початкової точки, з якої доступ доступний.

Перемога

Переможець - це відповідь, що забезпечує найкоротший дійсний рядок і включає код, який використовується для його створення. Якщо більш ніж один із відповідей містить рядок цієї найменшої довжини, перший, хто розмістив цю довжину, виграє.

Приклад

Ось приклад рядка довжиною 500 символів, щоб дати вам щось перемогти:

SEENSSNESSWNNSNNNNWWNWENENNWEENSESSNENSESWENWWWWWENWNWWSESNSWENNWNWENWSSSNNNNNNESWNEWWWWWNNNSWESSEEWNENWENEENNEEESEENSSEENNWWWNWSWNSSENNNWESSESNWESWEENNWSNWWEEWWESNWEEEWWSSSESEEWWNSSEEEEESSENWWNNSWNENSESSNEESENEWSSNWNSEWEEEWEESWSNNNEWNNWNWSSWEESSSSNESESNENNWEESNWEWSWNSNWNNWENSNSWEWSWWNNWNSENESSNENEWNSSWNNEWSESWENEEENSWWSNNNNSSNENEWSNEEWNWENEEWEESEWEEWSSESSSWNWNNSWNWENWNENWNSWESNWSNSSENENNNWSSENSSSWWNENWWWEWSEWSNSSWNNSEWEWENSWENWSENEENSWEWSEWWSESSWWWNWSSEWSNWSNNWESNSNENNSNEWSNNESNNENWNWNNNEWWEWEE

Дякуємо orlp за пожертвування цього.


Таблиця лідерів

Таблиця лідерів

Рівні бали перераховані в порядку розміщення цього балу. Це не обов'язково порядок розміщення відповідей, оскільки оцінка за дану відповідь може оновлюватися з часом.


Суддя

Ось валідатор Python 3, який приймає рядок NESW як аргумент командного рядка або через STDIN.

Для недійсного рядка це дає вам наочний приклад лабіринту, для якого він не вдається.


3
Це дійсно акуратне питання. Чи є один найкоротший рядок (або ряд рядків і доказ того, що не може бути коротших відповідей)? А якщо так, то чи знаєте ви це?
Alex Van Liew

1
@AlexReinking так, що початком може бути будь-яка з 9 комірок, а виходом може бути будь-яка з 9 комірок, якщо вони не є однією і тією ж коміркою, і вихід доступний із самого початку.
трихоплакс

1
Трохи схожий на це запитання про stackoverflow: stackoverflow.com/questions/26910401/… - але початкова та кінцева комірка вгорі ліворуч і знизу праворуч у тій, що зменшує можливе число лабіринтів до
2423.

1
У будь-якому випадку @proudhaskeller було б вагомим питанням. Загальний випадок, набраний для n = 3, потребував би більш узагальненого коду. Цей конкретний випадок дозволяє здійснити оптимізацію, яка не стосується загальної російської мови, і саме це я вирішив запитати.
трихоплакс

2
Хтось розглядав підхід до цієї проблеми як пошук найкоротшого прийнятого рядка до регулярного виразу? Було б потрібно багато скоротити кількість проблем перед переходом на регулярні вирази, але теоретично можна було б знайти оптимально вирішене рішення.
Кайл МакКормік

Відповіді:


37

C ++, 97 95 93 91 86 83 82 81 79 символів

NNWSWNNSENESESWSSWNSEENWWNWSSEWWNENWEENWSWNWSSENENWNWNESENESESWNWSESEWWENENNENE

Моя стратегія досить проста - алгоритм еволюції, який може рости, стискати, змінювати елементи та мутувати дійсні послідовності. Моя логіка еволюції зараз майже така ж, як і @ Sp3000, оскільки його було покращенням у порівнянні з моєю.

Однак моє реалізація логіки лабіринту досить витончене. Це дозволяє мені перевірити, чи строки дійсні при швидкості пухирців. Спробуйте розібратися, дивлячись на коментар do_moveта Mazeконструктор.

#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
Підтверджено дійсне. Я вражений - я не сподівався побачити рядки цього короткого.
трихоплакс

2
Нарешті я завершив встановлення gcc і запустив це для себе. Гіпнотично спостерігає, як мутації струн і повільно скорочуються ...
trichoplax

1
@trichoplax Я сказав вам, що це весело :)
orlp

2
@AlexReinking Я оновив свою відповідь зазначеною реалізацією. Якщо ви подивитесь на демонтаж, ви побачите, що це лише десяток інструкцій без будь-якої гілки або навантаження: coliru.stacked-crooked.com/a/3b09d36db85ce793 .
orlp

2
@AlexReinking Готово. do_moveзараз шалено швидко.
orlp

16

Python 3 + PyPy, 82 80 символів

SWWNNSENESESWSSWSEENWNWSWSEWNWNENENWWSESSEWSWNWSENWEENWWNNESENESSWNWSESESWWNNESE

Я вагався, як опублікувати цю відповідь, тому що в основному я застосував підхід Орлпа і поклав на нього свою спіну. Цей рядок був знайдений, починаючи з рішення псевдовипадкової довжини 500 - досить багато насіння було випробувано, перш ніж я міг побити поточний рекорд.

Єдина нова основна оптимізація - це те, що я дивлюся лише на третину лабіринтів. З пошуку виключаються дві категорії лабіринтів:

  • Лабіринти, де <= 7площі доступні
  • Лабіринти, де всі доступні квадрати знаходяться на одному шляху, а старт / фініш не на обох кінцях

Ідея полягає в тому, що будь-яка струна, яка вирішує решту лабіринтів, також повинна вирішувати вищезазначене автоматично. Я переконаний, що це стосується другого типу, але це точно не відповідає першому, тому вихід буде містити помилкові позитиви, які потрібно перевірити окремо. Ці помилкові позитиви, як правило, пропускають лише 20 лабіринтів, тому я подумав, що це буде хороший компроміс між швидкістю та точністю, і це також дасть струнам трохи більше місця для дихання для мутації.

Спочатку я пройшов довгий список пошукової евристики, але жахливо жодна з них не придумала нічого кращого за 140 чи більше.

import random

N, M = 3, 3

W = 2*N-1
H = 2*M-1

random.seed(142857)


def move(c, cell, walls):
    global W, H

    if c == "N":
        if cell > W and not (1<<(cell-W)//2 & walls):
            cell = cell - W*2

    elif c == "S":
        if cell < W*(H-1) and not (1<<(cell+W)//2 & walls):
            cell = cell + W*2

    elif c == "E":
        if cell % W < W-1 and not (1<<(cell+1)//2 & walls):
            cell = cell + 2

    elif c == "W":
        if cell % W > 0 and not (1<<(cell-1)//2 & walls):
            cell = cell - 2

    return cell


def valid_maze(start, finish, walls):
    global adjacent

    if start == finish:
        return False

    visited = set()
    cells = [start]

    while cells:
        curr_cell = cells.pop()

        if curr_cell == finish:
            return True

        if curr_cell in visited:
            continue

        visited.add(curr_cell)

        for c in "NSEW":
            cells.append(move(c, curr_cell, walls))

    return False


def print_maze(maze):
    start, finish, walls = maze
    print_str = "".join(" #"[walls & (1 << i//2) != 0] if i%2 == 1
                        else " SF"[2*(i==finish) + (i==start)]
                        for i in range(W*H))

    print("#"*(H+2))

    for i in range(H):
        print("#" + print_str[i*W:(i+1)*W] + "#")

    print("#"*(H+2), end="\n\n")

all_cells = [W*y+x for y in range(0, H, 2) for x in range(0, W, 2)]
mazes = []

for start in all_cells:
    for finish in all_cells:
        for walls in range(1<<(N*(M-1) + M*(N-1))):
            if valid_maze(start, finish, walls):
                mazes.append((start, finish, walls))

num_mazes = len(mazes)
print(num_mazes, "mazes generated")

to_remove = set()

for i, maze in enumerate(mazes):
    start, finish, walls = maze

    reachable = set()
    cells = [start]

    while cells:
        cell = cells.pop()

        if cell in reachable:
            continue

        reachable.add(cell)

        if cell == finish:
            continue

        for c in "NSEW":
            new_cell = move(c, cell, walls)
            cells.append(new_cell)

    max_deg = 0
    sf = set()

    for cell in reachable:
        deg = 0

        for c in "NSEW":
            if move(c, cell, walls) != cell:
                deg += 1

        max_deg = max(deg, max_deg)

        if deg == 1:
            sf.add(cell)

    if max_deg <= 2 and len(sf) == 2 and sf != {start, finish}:
        # Single path subset
        to_remove.add(i)

    elif len(reachable) <= (N*M*4)//5:
        # Low reachability maze, above ratio is adjustable
        to_remove.add(i)

mazes = [maze for i,maze in enumerate(mazes) if i not in to_remove]
print(num_mazes - len(mazes), "mazes removed,", len(mazes), "remaining")
num_mazes = len(mazes)


def check(string, cache = set()):
    global mazes

    if string in cache:
        return True

    for i, maze in enumerate(mazes):
        start, finish, walls = maze
        cell = start

        for c in string:
            cell = move(c, cell, walls)

            if cell == finish:
                break

        else:
            # Swap maze to front
            mazes[i//2], mazes[i] = mazes[i], mazes[i//2]
            return False

    cache.add(string)
    return True


while True:
    string = "".join(random.choice("NSEW") for _ in range(500))

    if check(string):
        break

# string = "NWWSSESNESESNNWNNSWNWSSENESWSWNENENWNWESESENNESWSESWNWSWNNEWSESWSEEWNENWWSSNNEESS"

best = len(string)
seen = set()

while True:
    action = random.random()

    if action < 0.1:
        # Grow
        num_grow = int(random.expovariate(lambd=3)) + 1
        new_string = string

        for _ in range(num_grow):
            i = random.randrange(len(new_string))
            new_string = new_string[:i] + random.choice("NSEW") + new_string[i:]

    elif action < 0.2:
        # Swap
        num_swap = int(random.expovariate(lambd=1)) + 1
        new_string = string

        for _ in range(num_swap):
            i,j = sorted(random.sample(range(len(new_string)), 2))
            new_string = new_string[:i] + new_string[j] + new_string[i+1:j] + new_string[i] + new_string[j+1:]

    elif action < 0.35:
        # Mutate
        num_mutate = int(random.expovariate(lambd=1)) + 1
        new_string = string

        for _ in range(num_mutate):
            i = random.randrange(len(new_string))
            new_string = new_string[:i] + random.choice("NSEW") + new_string[i+1:]

    else:
        # Shrink
        num_shrink = int(random.expovariate(lambd=3)) + 1
        new_string = string

        for _ in range(num_shrink):
            i = random.randrange(len(new_string))
            new_string = new_string[:i] + new_string[i+1:]


    if check(new_string):
        string = new_string

    if len(string) <= best and string not in seen:
        while True:
            if len(string) < best:
                seen = set()

            seen.add(string)
            best = len(string)
            print(string, len(string))

            # Force removals on new record strings
            for i in range(len(string)):
                new_string = string[:i] + string[i+1:]

                if check(new_string):
                    string = new_string
                    break

            else:
                break

Підтверджено дійсне. Приємні покращення :)
trichoplax

Мені подобається, що твоя ідея здійснення деяких лабіринтів не потрібно перевіряти. Не могли б ви якось автоматизувати процес визначення того, які лабіринти - це зайві перевірки? Мені цікаво дізнатись, чи виявиться це більше лабіринтів, ніж тих, які можна зрозуміти інтуїтивно ...
trichoplax

Які ваші міркування про те, що не потрібно перевіряти графіки контурів, де початок не з одного кінця? Випадок, коли фініш не на одному кінці, легко виправдати, і його можна посилити, щоб не потрібно перевіряти випадки, коли фініш є розрізаною вершиною, але я не можу зрозуміти, як виправдати усунення початкових вершин.
Пітер Тейлор

@PeterTaylor Подумавши, теоретично ти маєш рацію, є кілька лабіринтів, які ти не можеш усунути так. Однак, здається, що на 3x3 це не має значення для рядків.
orlp

2
@orlp, Sp3000 замальовував доказ у чаті. Графіки шляху - особливий випадок. Позначити клітини , 0щоб nпо шляху , і припустимо , що рядок Sотримує вас від 0до n. Потім Sтакож отримує вас з будь-якої проміжної комірки cдо n. Припустимо, інакше. Нехай a(i)буде позиція після iкроків, починаючи з 0та b(i)починаючи з c. Потім a(0) = 0 < b(0)кожен крок змінюється aі bмаксимум на 1, і a(|S|) = n > b(|S|). Візьміть найменший tтакий, що a(t) >= b(t). Зрозуміло, що a(t) != b(t)вони синхронізуються, тому вони повинні міняти місцями на кроці t, рухаючись в тому ж напрямку.
Пітер Тейлор

3

C ++ та бібліотека від тривалості

Короткий зміст: Новий підхід, відсутність нових рішень , приємна програма, з якою грати, та кілька цікавих результатів локальної неістовірності відомих рішень. О, і кілька загально корисних спостережень.

Використовуючи підхід, заснований на SAT , я міг повністю вирішити подібну проблему для лабіринтів 4x4 із заблокованими осередками замість тонких стінок та фіксованих позицій старту та виходу у протилежних кутах. Тож я сподівався, що зможу використовувати ті самі ідеї для цієї проблеми. Однак, хоча для іншої проблеми я використав лише 2423 лабіринти (тим часом було помічено, що 2083 вистачає), і вона має рішення довжиною 29, кодування SAT використовувало мільйони змінних, і на її вирішення пішло кілька днів.

Тому я вирішив змінити підхід двома важливими способами:

  • Не наполягайте на пошуку рішення з нуля, але дозвольте зафіксувати частину рядка рішення. (Це легко зробити все одно, додавши одиничні пропозиції, але моя програма робить це зручно.)
  • Не використовуйте всі лабіринти з самого початку. Натомість поступово додайте по черзі один невирішений лабіринт. Деякі лабіринти можуть бути вирішені випадково, або вони завжди вирішуються, коли вирішені вже розглянуті. В останньому випадку він ніколи не буде доданий, без того, щоб нам було потрібно знати наслідки.

Я також зробив деякі оптимізації для використання менших змінних та одиничних пропозицій.

Програма базується на @ orlp's. Важливою зміною став вибір лабіринтів:

  • Перш за все, лабіринти задаються лише їхньою будовою стіни та вихідним положенням. (Вони також зберігають доступні позиції.) Функція is_solutionперевіряє, чи досягнуто всіх доступних позицій.
  • (Без змін: досі не використовуються лабіринти лише з 4 або менш доступними позиціями. Але більшість з них все одно будуть викинуті наступними спостереженнями.)
  • Якщо в лабіринті не використовується жодна з трьох верхніх комірок, це рівносильно лабіринту, який зміщується вгору. Тож ми можемо її скинути. Так само і для лабіринту, який не використовує жодну з трьох лівих клітин.
  • Не має значення, якщо з'єднані недоступні деталі, тому ми наполягаємо на тому, щоб кожна недосяжна клітина була повністю оточена стінами.
  • Лабіринт з єдиним контуром, який є підматом більшого лабіринту одного шляху, завжди вирішується, коли вирішується більший, тому він нам не потрібен. Кожен лабіринт одного розміру не більше 7 є частиною більшого (як і раніше розміщений у 3х3), але є 8 лабіринтів з одиночним контуром, яких немає. Для спрощення давайте просто відкинемо лабіринти одного шляху розміром менше 8. (І я все ще використовую, що лише початкові позиції потрібно вважати вихідними положеннями. Усі позиції використовуються як вихідні позиції, що має значення лише для частини SAT програми.)

Таким чином я отримую в цілому 10772 лабіринти зі стартовими позиціями.

Ось програма:

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

По- перше , configure.shі решателя, а потім скомпілювати програму з чим - то начебто , де є шлях , де відповідно. є, так можуть бути, наприклад, обидва . Або просто помістіть їх в один і той же каталог і обійтися без параметрів і .makelingelingg++ -std=c++11 -O3 -I ... -o m3sat m3sat.cc -L ... -llgl...lglib.hliblgl.a../lingeling-<version>-I-L

Програма приймає один обов'язковий аргумент командного рядка, рядок , що складається з N, E, S, W(для фіксованих напрямків) або *. Таким чином, ви можете шукати загальне рішення розміром 78, давши рядок розміром 78 *с (у лапках) або шукати рішення, починаючи з NEWSвикористання NEWSнаступного стільки *s, скільки вам потрібно для додаткових кроків. В якості першого тесту візьміть улюблене рішення і замініть деякі букви *. Це швидко знаходить рішення для напрочуд високого значення "деяких".

Програма розповість, який лабіринт додає, описана структурою стіни та початковою позицією, а також дасть кількість доступних позицій та стін. Лабіринти сортуються за цими критеріями, і додається перший невирішений. Тому більшість доданих лабіринтів є (9/4), але іноді з'являються і інші.

Я взяв відоме рішення довжиною 79, і для кожної групи сусідніх 26 літер спробував замінити їх будь-якими 25 літерами. Я також намагався видалити 13 букв на початку та в кінці, і замінив їх будь-якими 13 на початку та будь-якими 12 в кінці, і навпаки. На жаль, все вийшло незадовільно. Отже, чи можемо ми вважати це показником того, що довжина 79 оптимальна? Ні, я аналогічно намагався поліпшити рішення довжиною 80 до довжини 79, і це також не було успішним.

Нарешті, я спробував поєднати початок одного рішення з кінцем іншого, а також з одним рішенням, трансформованим однією із симетрій. Зараз мені не вистачає цікавих ідей, тому я вирішив показати вам, що я маю, хоча це не призвело до нових рішень.


Це було дійсно цікаве прочитання. Як новий підхід, так і різні способи зменшення кількості лабіринтів для перевірки. Щоб бути правильною відповіддю, для цього потрібно буде включити дійсну рядок. Це не повинно бути новим найкоротшим рядком, а будь-який дійсний рядок будь-якої довжини, щоб дати поточну оцінку для цього підходу. Я згадую про це, оскільки без балів відповідь буде загрожує видаленням, і я дуже хотів би, щоб це залишилося.
трихоплакс

Також приємна робота з пошуку оптимального рішення довжини для більш старого завдання !
трихоплакс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.