Логическая игра, 9 квадратных карт и один большой квадрат - PullRequest
3 голосов
/ 20 февраля 2011

Я не уверен, что это правильное место, может быть, какой-то другой обмен стека, скажите мне, что я бы отправил куда-нибудь еще.

Вот моя проблема, я нашел старую игру у своих друзей,это должна быть интеллектуальная игра: 9 маленьких квадратных карточек, и вы должны поместить их так, чтобы они все сошлись вместе, вот картинка:

the game to solve

Через несколько часовПеред игрой я понял, что по-настоящему простого и честного способа закончить игру не было, и поэтому я пошел программным путем.

Это - то, где у меня возникают трудности, хотя я мог бы простоиспользовать некоторые случайные функции, большой цикл и покончить с этим.Но есть что-то вроде (4 * 9) ^ 9 решений, так что, кажется, это не так просто.

Вот код, который я написал, который на данный момент довольно бесполезен: каждый раз, когда я захожу в цикл, яперемешайте мой массив, поверните мои карты на случайную величину и проверьте правильность головоломки, много потраченных впустую циклов, но я не знаю, с чего начать, чтобы сделать ее более эффективной.

РЕДАКТИРОВАТЬ: Исправлен код, я получаю несколько колод с 8 картами, но нет колоды из 9 карт, если у кого-то есть исправление к моему коду, или, может быть, нет решения?

require 'json'

class Array
    def rotate n
        a =dup
        n.times do a << a.shift end
        a
    end
end


@grid = [[{"type"=>"p", "head" => 1},{"type"=>"c", "head" => 1},{"type"=>"a", "head" => 2},{"type"=>"o", "head" => 2}],
[{"type"=>"o", "head" => 1},{"type"=>"a", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"p", "head" => 1}],
[{"type"=>"c", "head" => 1},{"type"=>"p", "head" => 2},{"type"=>"o", "head" => 2},{"type"=>"a", "head" => 1}],
[{"type"=>"p", "head" => 1},{"type"=>"c", "head" => 2},{"type"=>"o", "head" => 2},{"type"=>"a", "head" => 1}],
[{"type"=>"p", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"a", "head" => 1},{"type"=>"c", "head" => 1}],
[{"type"=>"a", "head" => 1},{"type"=>"p", "head" => 2},{"type"=>"o", "head" => 2},{"type"=>"p", "head" => 1}],
[{"type"=>"a", "head" => 1},{"type"=>"o", "head" => 1},{"type"=>"a", "head" => 2},{"type"=>"c", "head" => 2}],
[{"type"=>"o", "head" => 1},{"type"=>"a", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"p", "head" => 1}],
[{"type"=>"p", "head" => 1},{"type"=>"c", "head" => 2},{"type"=>"o", "head" => 2},{"type"=>"a", "head" => 1}]]
@new_grid = [nil, nil, nil,nil, nil, nil,nil, nil, nil]
@used = [false, false, false,false, false, false,false, false, false]

def check_validity(card, position, orientation)
    # since I'm adding from top left to bottom, I only need to check top and left           
    try_card = @grid[card].rotate orientation     
    valid = true
    # top
    if (@new_grid[position-3])
        if (try_card[0]["type"] != @new_grid[position-3][2]["type"] || try_card[0]["head"] == @new_grid[position-3][2]["head"])
            valid = false
        end
    end
    # left
    if (@new_grid[position-1] && (position % 3) != 0)
        if (try_card[3]["type"] != @new_grid[position-1][1]["type"] || try_card[3]["head"] == @new_grid[position-1][1]["head"])
            valid = false       
        end
    end
    return valid
end


def solve_puzzle(position)
    (0..8).each do |card|
        unless (@used[card])
            (0..3).each do |orientation|
                if (check_validity(card, position, orientation))
                    @used[card] = true
                    @new_grid[position] = @grid[card].rotate orientation
                                        if position == 7 
                                            puts @new_grid.to_json                                      
                                        end
                    if (position < 8)
                        solve_puzzle(position + 1)
                    else
                        puts "I WON"
                        puts @new_grid.to_json
                    end
                    @new_grid[position] = nil
                    @used[card] = false
                end
            end
        end
    end
end


solve_puzzle(0)

Ответы [ 6 ]

2 голосов
/ 21 февраля 2011

До недавнего времени я занимался исследованиями в области программирования с ограничениями, и думаю, что могу дать несколько советов.

Ваш лучший вариант - попытаться сгенерировать и протестировать с некоторой разумной эвристикой поиска и небольшой хитростью, чтобы минимизировать количество потерянных поисков.

Подумайте о проблеме следующим образом: у вас есть девять логических переменных, которые вы хотите назначить, {x1, ..., x9}, где x1, x2, x3 - нижний ряд, x4, x5, x6 - средний ряд, и x7, x8, x9 верхний ряд.

Каждая переменная может принимать одно из тридцати шести возможных значений из набора D = {(p, r) | p это кусок {p1, p2, ..., p9} и r это вращение {0, 90, 180, 270}}.

Решением является присвоение x1, ..., x9 из D так, что каждый фрагмент используется ровно в одном назначении, а каждая пара соседних фрагментов имеет совместимые назначения (т. Е. Ребра совпадают).

Ваш поиск должен отслеживать область возможных назначений для каждой переменной. В частности:

  • если вы назначите часть pi переменной xj, то вы должны вычеркнуть все возможные значения, содержащие pi, из областей всех других переменных;
  • если вы присваиваете значение (pi, r) переменной xj, то вы должны удалить все несовместимые присваивания из соседей xj;
  • если вы когда-либо удаляете все возможные назначения из домена переменной, то вы знаете, что попали в тупик и должны вернуться назад;
  • если вы когда-нибудь уменьшите набор возможных назначений для переменной до одного значения, то вы знаете, что значение должно быть присвоено этой переменной;
  • если вы хотите проявить фантазию, вы можете использовать обратный прыжок, а не простой возврат (здесь вы возвращаетесь в случае неудачи самого последнего конфликтующего решения, которое мешает вам присвоить переменную, а не просто возвращаете назад непосредственно предшествующее решение) .

Хорошая стратегия поиска - всегда выбирать переменную с наименьшим оставшимся доменом, чтобы попытаться назначить следующую. Таким образом, вы всегда смотрите на переменные, которые больше всего пострадали от решений, которые вы уже приняли в поисковой ветви.

В любом случае, надеюсь, это поможет.

ура!

2 голосов
/ 20 февраля 2011

Используйте рекурсию с обрезкой.Я имею в виду, когда вы кладете текущую карту, она должна соответствовать ориентации карт, которые вы уже положили.Таким образом, вы устраняете много невозможных ситуаций :)

Вот так:

    void generate(int whichPos) //whichPos is from 1 to 9
    {
       for (int card = 1; card <= 9; card++)
       {
          if (used[card]) continue;

          for (int orientation = 0; orientation < 4; orientation++)
          {
              if (orientation does match other cards from 1 to whichPos - 1 in the grid)
              { 
                  used[card] = true;
                  saveInGrid();
                  generate(whichPos + 1);
                  used[card] = false;
              }       
          }  
        }
    }

    generate(1);
1 голос
/ 23 февраля 2011

Эта проблема представляет собой еще одну форму мозаичной головоломки , другие формы которой включают Пентомино и Содуку .

Одним из наиболее эффективных алгоритмов для решения таких головоломок является Алгоритм X , разработанный Дональдом Кнутом . Один из лучших методов реализации Алгоритма X известен как Dancing Links .

Основная причина, по которой Алгоритм X невероятно эффективен в решении головоломок (типично решение Suduku за несколько миллисекунд), заключается в том, что бесполезные конфигурации эффективно удаляются из пространства поиска.

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

С точки зрения формулировки матрицы ограничений для Алгоритма X у вас есть 9 фигур, каждая из которых может быть размещена в 9 местах с 4 ориентациями, поэтому ваша матрица ограничений будет иметь 324 строки, по одной на каждое возможное размещение фигур.

Определение столбцов ограничения более сложное. Вам понадобится 9 столбцов ограничений - по одному для каждого местоположения, чтобы гарантировать, что части не расположены вместе. Вам также понадобится несколько дополнительных столбцов, чтобы зафиксировать ограничение на совместимость символов, пересекающих каждое ребро. Есть двенадцать точек взаимодействия, четыре символа каждый разделен на две части - еще 96 столбцов.

Обновление

Вот укол при запуске матрицы ограничений. Давайте возьмем первый фрагмент, показанный на картинке OP, который состоит из четырех частей:

  • Голова Хетафикса (Север)
  • Римская голова (Восток)
  • Астерикс Фут (Юг)
  • Обеликс Фут (Запад)

Для нашей матрицы ограничений нам нужно

  • 9 столбцов для позиций (TopLeft, TopCentre, ... MiddleCentre, ... BottomRight)
  • 9 колонок для штук (Piece1 ... Piece9)
  • 96 столбцов для точек взаимодействия

Мы строим строку с ограничениями, перечисляя как то, что мы получаем из определенной позиции, так и то, что эта позиция предотвращает.

  • Расположение: TopLeft, Piece1
  • Включает в себя: TopLeftEast-Roman-Head, TopLeftSouth-Asterix-Feet
  • Не включает в себя: TopCentreWest-Getafix-Head, TopCentreWest-Getafix-Feet, TopCentreWest-Obelix-Head, TopCentreWest-Obelix-Feet, TopCentreWest-Asterix-Feet, TopCentreWest-Asterix-Head *, TopCentreWest-Head, 10 RomanSWW 10 (По сути, в TopCentreWest нормально только с римскими ногами)
  • Не включает: средне-лево-северно-обеликсовая головка, средне-лево-северно-обеликс-футы, средне-лево-северно-гетафиксальные головки, средне-лево-северно-гетафиксированные ноги, средне-лево-северно-римские головы, средне-лево-северно-римские ноги, средне-лево-северные-астерикс * 1063 (Только в средней звездочке все в порядке на MiddleLeftNorth)

Теперь повторите это еще 3 раза для остальных трех ориентаций этого фрагмента в локации TopLeft ...

  • Расположение: TopLeft, Piece1
  • Включает в себя: TopLeftEast-Getafix-Head, TopLeftSouth-Roman-Head
  • ...

  • Местоположение: TopLeft, Piece1

  • Включает в себя: TopLeftEast-Obelix-Feet, TopLeftSouth-Getafix-Head
  • ...

  • Расположение: TopLeft, Piece1

  • Включает в себя: TopLeftEast-Asterix-Feet, TopLeftSouth-ObelixFeet
  • ...

Повторите эти 4 строки еще 8 раз для каждой из 4 ориентаций в 8 оставшихся пространствах (еще 32 строки, всего 36 строк).

Затем создайте еще 36 строк для каждого местоположения / ориентации оставшихся 8 частей (еще 288 строк).

Возможно, вы захотите написать код для генерации матрицы, а не вычислять ее вручную!

1 голос
/ 23 февраля 2011

На самом деле не существует решения для квадратов на изображении.Я заставил это работать, изменив зеленое / синее основание на правой стороне последнего части к красному / белому основанию (как у части выше это имеет справа).

Рандомизация - плохая идея, этоЛучше сделать исчерпывающий поиск, где вы попробуете любую возможность, как ответил Петр Минчев.Вы можете ускорить его, предварительно обработав.За исключением первого фрагмента, вы всегда будете знать одну или две стороны, которые должны соответствовать.Каждый тип имеет только от 3 до 6 экземпляров среди всех 9 штук.Представьте, что у вас есть следующие пробелы:

0   1   2
3   4   5
6   7   8

Первоначально я сделал это, заполнив позицию 4, затем 1, 3, 5, 7 и, наконец, углы.Это нашло 4 решения за 3,5 миллисекунды с 2371 рекурсивными вызовами.Изменив порядок на 4, 1, 5, 2, 7, 8, 3, 0, 6, он упал до 1,2 миллисекунды только с 813 рекурсивными вызовами, потому что было меньше вариантов для размещения в углах.Если подумать об этом сейчас, то порядок будет где-то посередине, но измененный порядок будет таким же быстрым (0, 1, 3, 4, 2, 5, 6, 7, 8).Важно то, что проверка двух совпадений уменьшает количество повторных вызовов.

Step[] Steps = {
    new Step() { Type = 0, Position = 4 },
    new Step() { Type = 1, Position = 1, MatchP1 = 4, MatchO1 = 0 },
    new Step() { Type = 1, Position = 5, MatchP1 = 4, MatchO1 = 1 },
    new Step() { Type = 2, Position = 2, MatchP1 = 5, MatchO1 = 0, MatchP2 = 1, MatchO2 = 1 },
    new Step() { Type = 1, Position = 7, MatchP1 = 4, MatchO1 = 2 },
    new Step() { Type = 2, Position = 8, MatchP1 = 7, MatchO1 = 1, MatchP2 = 5, MatchO2 = 2 },
    new Step() { Type = 1, Position = 3, MatchP1 = 4, MatchO1 = 3 },
    new Step() { Type = 2, Position = 0, MatchP1 = 1, MatchO1 = 3, MatchP2 = 3, MatchO2 = 0 },
    new Step() { Type = 2, Position = 6, MatchP1 = 3, MatchO1 = 2, MatchP2 = 7, MatchO2 = 3 },
};

Вот как настроены мои карты, обратите внимание, что я поменял одну из сторон напоследняя карта, чтобы получить решение.1 - красный, 2 - толстый, 3 - синий / зеленый и 4 - желтый.Я xor с 0x10, чтобы показать, что это низ того же цвета.Таким образом, вы можете xor 2 типа и сравнить с 0x10, чтобы увидеть, если они совпадают, или вы можете xor типа с 0x10, чтобы найти тип, который вы ищете.

Card[] cards = {
    new Card(0x01, 0x03, 0x14, 0x12),
    new Card(0x02, 0x14, 0x13, 0x01),
    new Card(0x03, 0x11, 0x12, 0x04),

    new Card(0x01, 0x13, 0x12, 0x04),
    new Card(0x11, 0x13, 0x04, 0x03),
    new Card(0x04, 0x11, 0x12, 0x01),

    new Card(0x04, 0x02, 0x14, 0x13),
    new Card(0x02, 0x14, 0x13, 0x01),
//              new Card(0x01, 0x13, 0x12, 0x04) // no solution
    new Card(0x01, 0x11, 0x12, 0x04) // 4 solutions
};

При предварительной обработке, я хочуУ меня есть массив, индексированный по типу, который я ищу, который даст мне все карты с этим типом и в какой ориентации этот тип включен.Я также добавляю NextType (по часовой стрелке), чтобы упростить сравнение углов:

public CardImageOrientation[][] Orientations { get; set; }

public struct CardImageOrientation
{
    public int CardIndex;
    public int TypePosition;
    public int NextType;
}

// Orientations[1] is an array of CardImageOrientation structs
// that tell me what cards contain a side with type 1, what position
// it is in, and what the next type clockwise is

Вот мой основной рекурсивный метод:

public bool Method1Step(int stepIndex)
{
    StepCalls++;
    if (stepIndex > 8) // found a match
    {
        FindCount++;
        return !Exhaustive; // false return value will keep going if exhaustive flag is true
    }

    Step step = Steps[stepIndex];
    switch (step.Type)
    {
        case 0:
            // step 0 we just loop through all cards and try them in position 4 with orientation 0
            for (int i = 0; i < 9; i++)
            {
                PlaceCard(i, 4, 0);
                steppedUp = true;
                if (Method1Step(stepIndex + 1))
                    // found a solution, return true (will be false below for exhaustive)
                    return true;
                RemoveCard(4);
            }
            break;
        case 1:
        case 2:
            // step type 1 we simply try to match one edge with another, find card in position we are matching with
            Card card = Cards[CardIndices[step.MatchP1]];

            // to find the actual type in that position, we take the position we are looking for, subtract card orientation, add 4 and take the lowest two bits
            int type = card.Types[(step.MatchO1 - card.Orientation + 4) & 0x03];

            // find opposite orientation where we need to put the match in the empty spot
            int orientation2 = (step.MatchO1 + 2) & 0x03;

            // looking for type that is the opposite of the existing type
            int searchType = type ^ 0x10;
            for (int i = 0; i < Orientations[searchType].Length; i++)
            {
                // try one card value that matches
                CardImageOrientation cio = Orientations[searchType][i];

                // make sure it isn't in use
                if (Cards[cio.CardIndex].Position < 0)
                {
                    // check either we are step 1 or that second type matches as well
                    if (step.Type == 1 || (
                            step.Type == 2 && 
                            (Cards[CardIndices[step.MatchP2]].Types[(step.MatchO2 - Cards[CardIndices[step.MatchP2]].Orientation + 4) & 0x3] ^ cio.NextType) == 0x10)
                        ) {
                        // get new orientation for card
                        int newOrientation = (orientation2 - cio.TypePosition + 4) & 0x03;

                        PlaceCard(cio.CardIndex, step.Position, newOrientation);
                        if (Method1Step(stepIndex + 1))
                            // found solution and non-exhaustive so return true
                            return true;
                        RemoveCard(step.Position);
                    }
                }
            }
            break;
    }
    return false; // dead end or exhaustive search
}

Интересное примечание: я написал код для рандомизациикарты, использующие существующую модифицированную колоду с 4 решениями и 9999 других колод, созданных со случайными начальными значениями между 1 и 9999. Я нашел 3465 решений, распределенных по 1555 начальным макетам за 7,2 секунды, так что в среднем он составлял около 0,72 миллисекунды за цикл.В общей сложности было совершено 4,16 миллиона вызовов метода Method1Step ().

1 голос
/ 21 февраля 2011

Я думаю, что ваша ошибка в проверке правильности. Вам не нужно проверять левую сторону позиций 3 и 6, поскольку они являются левой стороной головоломки и не должны совпадать с правой стороной предыдущего ряда.

Редактировать: вот строка, о которой я думаю:

# left
if (@new_grid[position-1] && (position % 3) != 0)
    if (try_card[3]["type"] != @new_grid[position-1][1]["type"] || try_card[3]["head"] == @new_grid[position-1][1]["head"])
        valid = false       
    end
end

Редактировать 2: Проверьте ваши фигуры, я вижу следующее для центральной фигуры:

[{"type"=>"p", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"a", "head" => 1},{"type"=>"c", "head" => 2}],

который я считаю, должен быть

[{"type"=>"p", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"a", "head" => 1},{"type"=>"c", "head" => 1}],
0 голосов
/ 21 февраля 2011

Я уверен, что знание пролога очень помогло бы здесь, но я не думаю, что могу ответить на этот вопрос.Однако я могу помочь вам понять, насколько вероятно ваше текущее решение:

9 частей можно разместить в 9!= 362880 перестановок (только положение)

Каждый из них может вращаться 4-мя способами -> 4 ^ 9 = 262114

Однако у вас будет 4 решения, которые являются просто вращениями друг друга, поэтому / =4.

Предоставление в среднем 23 781 703 680 возможных вариантов попыток, прежде чем вы найдете решение: (

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...