Проблема ACM: бросание монет, помогите мне определить тип проблемы, это - PullRequest
10 голосов
/ 24 октября 2008

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

Проблема заключается в следующем:


У вас есть головоломка, состоящая из квадратной сетки размером 4. Каждый квадрат сетки содержит одну монету; каждая монета показывает головы (H) и хвосты (T). Одна такая загадка показана здесь:

H H H H
Т Т Т Т
H T H T
T T H T

Любая монета с текущим хвостом (T) может быть подброшена к Головам (H). Однако всякий раз, когда мы подбрасываем монету, мы также должны переворачивать смежные монеты прямо выше, ниже и влево и вправо в одном ряду. Таким образом, если мы перевернем вторую монетку во втором ряду, мы также должны перевернуть еще 4 монеты, предоставив нам такое расположение (измененные монеты выделены жирным шрифтом).

В Т В В
H H H T
В В В Т
T T H T

Если монета находится на краю головоломки, поэтому на одной стороне или другой монеты нет, тогда мы подбрасываем меньше монет. Мы не «оборачиваемся» на другую сторону. Например, если мы перевернем нижнюю правую монету вышеупомянутого аргумента, мы получим:

H T H H
В В В В
В В В В
T T T H

Примечание. Для подбрасывания можно выбрать только монеты с хвостами (T). Однако каждый раз, когда мы подбрасываем такую ​​монету, смежные монеты также подбрасываются независимо от их состояния.

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

Например, сетка:
H T H H
T T T H
В Т В В
H H T T

Ответ на эту сетку: 2 сальто.


Что я сделал до сих пор:

Я храню наши сетки как двумерный массив логических значений. Головы = правда, хвосты = ложь. У меня есть метод flip (int row, int col) , который перевернет смежные монеты в соответствии с правилами, приведенными выше, и у меня есть метод isSolved () , который определит, находится ли загадка в решенное состояние (все главы). Итак, у нас есть «механика».

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

Ответы [ 10 ]

9 голосов
/ 24 октября 2008

Ваша головоломка - классический Поиск в ширину кандидат. Это потому, что вы ищете решение с наименьшим количеством возможных ходов.

Если бы вы знали количество ходов к цели, то это было бы идеально для поиска в глубину .

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

Любой поиск может быть рекурсивным, если вы уверены, что вам не хватит места в стеке.

6 голосов
/ 24 октября 2008

РЕДАКТИРОВАТЬ: я не заметил, что вы не можете использовать монету в качестве основного движения, если она не показывает хвосты. Это действительно делает порядок важным. Я оставлю этот ответ здесь, но также постараюсь написать еще один.

Здесь нет псевдокода, но подумайте об этом: можете ли вы когда-нибудь себе представить, что подбрасываете монету дважды? Какой будет эффект?

Альтернатива, запишите какую-нибудь произвольную доску (буквально, запишите ее). Установите несколько монет реального мира и выберите две произвольные, X и Y. Сделайте «X-flip», затем «Y-flip», затем другой «X-flip». Запишите результат. Теперь сбросьте доску на начальную версию и просто выполните «Y flip». Сравните результаты и подумайте о том, что произошло. Попробуйте несколько раз, иногда с X и Y близко друг к другу, иногда нет. Будьте уверены в своем заключении.

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

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

Что касается рекурсии: вы могли бы использовать рекурсию. Лично я бы не стал в этом случае.

РЕДАКТИРОВАТЬ: На самом деле, если подумать, я, вероятно, использовал бы рекурсию. Это может сделать жизнь намного проще.


Хорошо, возможно, этого было недостаточно очевидно. Давайте пометим монеты A-P следующим образом:

ABCD
EFGH
IJKL
MNOP

Flipping F всегда будет включать в себя следующие монеты, меняющие состояние: BEFGJ.

При подбрасывании J всегда изменяются следующие состояния монет: FIJKN.

Что произойдет, если вы подбросите монетку дважды? Два сальто отменяют друг друга, независимо от того, какие другие смещения происходят.

Другими словами, переключение F, а затем J - это то же самое, что переключение J, а затем F. Переключение F, а затем J, а затем снова F - это то же самое, что просто переключить J для начала.

Таким образом, любое решение на самом деле не является путем «переверни A, затем F, затем J» - это «переверни <эти монеты>; не переворачивай <эти монеты>». (К сожалению, слово «подбрасывание» используется как для переворачивания основной монеты, так и для вторичных монет, которые меняют состояние для конкретного хода, но не берите в голову - надеюсь, понятно, что я имею в виду.)

Каждая монета будет использоваться в качестве основного хода или нет, 0 или 1. Есть 16 монет, так что 2 ^ 16 возможностей. Таким образом, 0 может представлять «ничего не делать»; 1 может представлять «просто А»; 2 может представлять «просто B»; 3 "А и В" и т. Д.

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

Подсказка к реализации: «текущее состояние» также может быть представлено в виде 16-битного числа. Использование конкретной монеты в качестве основного хода всегда будет XOR текущего состояния с фиксированным номером (для этой монеты). Это позволяет легко определить эффект от любой конкретной комбинации ходов.


Хорошо, вот решение в C #. Он показывает, сколько ходов потребовалось для каждого решения, которое он находит, но не отслеживает, какие это были ходы или какое наименьшее количество ходов. Это SMOP:)

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

using System;

public class CoinFlip
{
    // All ints could really be ushorts, but ints are easier 
    // to work with
    static readonly int[] MoveTransitions = CalculateMoveTransitions();

    static int[] CalculateMoveTransitions()
    {
        int[] ret = new int[16];
        for (int i=0; i < 16; i++)
        {
            int row = i / 4;
            int col = i % 4;
            ret[i] = PositionToBit(row, col) +
                PositionToBit(row-1, col) +
                PositionToBit(row+1, col) +
                PositionToBit(row, col-1) +
                PositionToBit(row, col+1);
        }
        return ret;
    }

    static int PositionToBit(int row, int col)
    {
        if (row < 0 || row > 3 || col < 0 || col > 3)
        {
            // Makes edge detection easier
            return 0;
        }
        return 1 << (row * 4 + col);
    }

    static void Main(string[] args)
    {
        int initial = 0;
        foreach (char c in args[0])
        {
            initial += 1 << (c-'A');
        }
        Console.WriteLine("Initial = {0}", initial);
        ChangeState(initial, 0, 0);
    }

    static void ChangeState(int current, int nextCoin, int currentFlips)
    {
        // Reached the end. Success?
        if (nextCoin == 16)
        {
            if (current == 0)
            {
                // More work required if we want to display the solution :)
                Console.WriteLine("Found solution with {0} flips", currentFlips);
            }
        }
        else
        {
            // Don't flip this coin
            ChangeState(current, nextCoin+1, currentFlips);
            // Or do...
            ChangeState(current ^ MoveTransitions[nextCoin], nextCoin+1, currentFlips+1);
        }
    }
}
4 голосов
/ 25 октября 2008

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

Большой секрет здесь состоит в том, чтобы иметь несколько копий игрового поля. Не думай о «доске».

Я предлагаю создать структуру данных, которая содержит представление доски и упорядоченный список ходов, которые попали на эту доску из начальной позиции. Ход - это координаты центральной монеты в серии сальто. Я назову экземпляр этой структуры данных «состоянием» ниже.

Мой основной алгоритм будет выглядеть примерно так:

Create a queue.
Create a state that contains the start position and an empty list of moves.
Put this state into the queue.
Loop forever:
    Pull first state off of queue.
    For each coin showing tails on the board:
        Create a new state by flipping that coin and the appropriate others around it.
        Add the coordinates of that coin to the list of moves in the new state.
        If the new state shows all heads:
            Rejoice, you are done.
        Push the new state into the end of the queue.

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

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

2 голосов
/ 25 октября 2008

Хорошо, вот ответ, когда я правильно прочитал правила:)

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

Эта реализация создает много строк - неизменный связанный список ходов был бы более точным на этом фронте, но у меня сейчас нет времени на это.

using System;
using System.Collections.Generic;

public class CoinFlip
{
    struct Position
    {
        readonly string moves;
        readonly int state;

        public Position(string moves, int state)
        {
            this.moves = moves;
            this.state = state;
        }

        public string Moves { get { return moves; } } 
        public int State { get { return state; } }

        public IEnumerable<Position> GetNextPositions()
        {
            for (int move = 0; move < 16; move++)
            {
                if ((state & (1 << move)) == 0)
                {                    
                    continue; // Not allowed - it's already heads
                }
                int newState = state ^ MoveTransitions[move];
                yield return new Position(moves + (char)(move+'A'), newState);
            }
        }
    }

    // All ints could really be ushorts, but ints are easier 
    // to work with
    static readonly int[] MoveTransitions = CalculateMoveTransitions();

    static int[] CalculateMoveTransitions()
    {
        int[] ret = new int[16];
        for (int i=0; i < 16; i++)
        {
            int row = i / 4;
            int col = i % 4;
            ret[i] = PositionToBit(row, col) +
                PositionToBit(row-1, col) +
                PositionToBit(row+1, col) +
                PositionToBit(row, col-1) +
                PositionToBit(row, col+1);
        }
        return ret;
    }

    static int PositionToBit(int row, int col)
    {
        if (row < 0 || row > 3 || col < 0 || col > 3)
        {
            return 0;
        }
        return 1 << (row * 4 + col);
    }

    static void Main(string[] args)
    {
        int initial = 0;
        foreach (char c in args[0])
        {
            initial += 1 << (c-'A');
        }

        int maxDepth = int.Parse(args[1]);

        Queue<Position> queue = new Queue<Position>();
        queue.Enqueue(new Position("", initial));

        while (queue.Count != 0)
        {
            Position current = queue.Dequeue();
            if (current.State == 0)
            {
                Console.WriteLine("Found solution in {0} moves: {1}",
                                  current.Moves.Length, current.Moves);
                return;
            }
            if (current.Moves.Length == maxDepth)
            {
                continue;
            }
            // Shame Queue<T> doesn't have EnqueueRange :(
            foreach (Position nextPosition in current.GetNextPositions())
            {
                queue.Enqueue(nextPosition);
            }
        }
        Console.WriteLine("No solutions");
    }
}
2 голосов
/ 25 октября 2008

Чтобы уточнить предложение Федерико, проблема состоит в том, чтобы найти набор из 16 генераторов, которые xor'ed вместе дают стартовую позицию.

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

EDIT: Подумав немного больше, я думаю, что это сработает: Создайте двоичную матрицу G из всех генераторов, и пусть s будет начальным состоянием. Мы ищем векторы x, удовлетворяющие Gx=s (мод 2). После устранения гауссовости мы либо получим такой вектор x, либо обнаружим, что решений нет.

Задача состоит в том, чтобы найти вектор y такой, чтобы Gy = 0 и x^y имели как можно меньше установленных битов, и я думаю, что самый простой способ найти это - попробовать все такие y. Поскольку они зависят только от G, они могут быть предварительно вычислены.

Я признаю, что поиск методом "грубой силы" будет намного проще осуществить. =)

2 голосов
/ 25 октября 2008

Сетка, читаемая в главном порядке строк, является не более чем 16-битным целым числом. Как сетка, заданная задачей , так и , 16 возможных ходов (или «генераторов») могут быть сохранены как 16-битные целые числа, таким образом, проблема сводится к тому, чтобы найти наименьшее возможное количество генераторов, которое суммируется с помощью побитовое XOR, дает саму сетку как результат. Интересно, есть ли более разумная альтернатива, чем пробовать все 65536 возможностей.

РЕДАКТИРОВАТЬ: Действительно есть удобный способ сделать брутфорс. Вы можете попробовать все паттерны с 1 ходом, затем все паттерны с 2 ходами и так далее. Когда шаблон n-ходов соответствует сетке, вы можете остановиться, показать шаблон выигрыша и сказать, что для решения требуется как минимум n ходов. Перечисление всех n-ходовых паттернов является рекурсивной проблемой.

РЕДАКТИРОВАТЬ 2: Вы можете грубо с чем-то в соответствии с рекурсивным псевдокодом (возможно, с ошибками):

// Tries all the n bit patterns with k bits set to 1
tryAllPatterns(unsigned short n, unsigned short k, unsigned short commonAddend=0)
{
    if(n == 0)
        tryPattern(commonAddend);
    else
    {
        // All the patterns that have the n-th bit set to 1 and k-1 bits
        // set to 1 in the remaining
        tryAllPatterns(n-1, k-1, (2^(n-1) xor commonAddend) );

        // All the patterns that have the n-th bit set to 0 and k bits
        // set to 1 in the remaining
        tryAllPatterns(n-1, k,   commonAddend );
    }
}
1 голос
/ 24 февраля 2010

Классическая проблема "Lights Out". На самом деле существует простое O(2^N) решение для грубой силы, где N - это ширина или высота, в зависимости от того, что меньше.

Предположим, что следующие ширины работают с шириной, поскольку вы можете транспонировать ее.

Одно замечание: вам не нужно нажимать одну и ту же кнопку дважды - она ​​просто отменяется.

Ключевая концепция заключается в том, что вам нужно только определить, хотите ли вы нажимать кнопку для каждого элемента в первом ряду. Любое другое нажатие кнопки однозначно определяется одним - горит ли индикатор над рассматриваемой кнопкой. Если вы смотрите на ячейку (x,y), а ячейка (x,y-1) включена, есть только один способ ее выключить, нажав (x,y). Перебирайте строки сверху вниз, и если в конце не останется света, у вас есть решение. Затем вы можете взять минимум всех попыток.

1 голос
/ 13 марта 2009

Если вы практикуете в ACM, я бы рассмотрел эту головоломку и для нетривиальных досок, скажем, 1000x1000. Грубая сила / жадность могут все еще работать, но будьте осторожны, чтобы избежать экспоненциального взрыва.

0 голосов
/ 14 октября 2009

Я сел и попытался найти собственное решение этой проблемы (основываясь на помощи, которую я получил в этой теме). Я использую 2d массив логических значений, поэтому он не так хорош, как люди, использующие 16-битные целые числа с битовыми манипуляциями.

В любом случае, вот мое решение на Java:

import java.util.*;

class Node
{
    public boolean[][] Value;
    public Node Parent;

    public Node (boolean[][] value, Node parent)
    {
        this.Value = value;
        this.Parent = parent;
    }
}


public class CoinFlip
{
    public static void main(String[] args)
    {
        boolean[][] startState =  {{true, false, true, true},
                                   {false, false, false, true},
                                   {true, false, true, false},
                                   {true, true, false, false}};


        List<boolean[][]> solutionPath = search(startState);

        System.out.println("Solution Depth: " + solutionPath.size());
        for(int i = 0; i < solutionPath.size(); i++)
        {
            System.out.println("Transition " + (i+1) + ":");
            print2DArray(solutionPath.get(i));
        }

    }

    public static List<boolean[][]> search(boolean[][] startState)
    {
        Queue<Node> Open = new LinkedList<Node>();
        Queue<Node> Closed = new LinkedList<Node>();

        Node StartNode = new Node(startState, null);
        Open.add(StartNode);

          while(!Open.isEmpty())
          {
              Node nextState = Open.remove();

              System.out.println("Considering: ");
              print2DArray(nextState.Value);

              if (isComplete(nextState.Value))
              {
                  System.out.println("Solution Found!");
                  return constructPath(nextState);
              }
              else
              {
                List<Node> children = generateChildren(nextState);
                Closed.add(nextState);

                for(Node child : children)
                {
                    if (!Open.contains(child))
                        Open.add(child);
                }
              }

          }

          return new ArrayList<boolean[][]>();

    }

    public static List<boolean[][]> constructPath(Node node)
    {
        List<boolean[][]> solutionPath = new ArrayList<boolean[][]>();

        while(node.Parent != null)
        {
            solutionPath.add(node.Value);
            node = node.Parent;
        }
        Collections.reverse(solutionPath);

        return solutionPath;
    }

    public static List<Node> generateChildren(Node parent)
    {
        System.out.println("Generating Children...");
        List<Node> children = new ArrayList<Node>();

        boolean[][] coinState = parent.Value;

        for(int i = 0; i < coinState.length; i++)
        {
            for(int j = 0; j < coinState[i].length; j++)
            {
                if (!coinState[i][j])
                {
                    boolean[][] child = arrayDeepCopy(coinState);
                    flip(child, i, j);
                    children.add(new Node(child, parent));

                }
            }
        }

        return children;
    }

    public static boolean[][] arrayDeepCopy(boolean[][] original)
    {
         boolean[][] r = new boolean[original.length][original[0].length];
         for(int i=0; i < original.length; i++)
                 for (int j=0; j < original[0].length; j++)
                       r[i][j] = original[i][j];

         return r;
    }

    public static void flip(boolean[][] grid, int i, int j)
    {
        //System.out.println("Flip("+i+","+j+")");
        // if (i,j) is on the grid, and it is tails
        if ((i >= 0 && i < grid.length) && (j >= 0 && j <= grid[i].length))
        {
            // flip (i,j)
            grid[i][j] = !grid[i][j];
            // flip 1 to the right
            if (i+1 >= 0 && i+1 < grid.length) grid[i+1][j] = !grid[i+1][j];
            // flip 1 down
            if (j+1 >= 0 && j+1 < grid[i].length) grid[i][j+1] = !grid[i][j+1];
            // flip 1 to the left
            if (i-1 >= 0 && i-1 < grid.length) grid[i-1][j] = !grid[i-1][j];
            // flip 1 up
            if (j-1 >= 0 && j-1 < grid[i].length) grid[i][j-1] = !grid[i][j-1];
        }
    }

    public static boolean isComplete(boolean[][] coins)
    {
        boolean complete = true;

        for(int i = 0; i < coins.length; i++)
        {
            for(int j = 0; j < coins[i].length; j++)
            {
                if (coins[i][j] == false) complete = false; 
            }

        }
        return complete;
    }

    public static void print2DArray(boolean[][] array) 
    {
        for (int row=0; row < array.length; row++) 
        {
            for (int col=0; col < array[row].length; col++)
            {
                System.out.print((array[row][col] ? "H" : "T") + " ");
            }
            System.out.println();
        }
    }

}
0 голосов
/ 25 октября 2008

Это конечный автомат , где каждое «состояние» - это 16-битное целое число, соответствующее значению каждой монеты.

Каждое состояние имеет 16 исходящих переходов, соответствующих состоянию после подбрасывания каждой монеты.

Как только вы наметили все состояния и переходы, вы должны найти кратчайший путь на графике от вашего начального состояния до состояния 1111 1111 1111 1111,

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