Игра с подбрасыванием монет: проблема оптимизации

Существует прямоугольная сетка монет, где решка представлена ​​значением 1, а решка представлена ​​значением 0. Вы представляете это с помощью таблицы двумерного целочисленного массива (от 1 до 10 строк/столбцов включительно).

На каждом ходу вы выбираете любую отдельную ячейку (R, C) в сетке (R-я строка, C-й столбец) и подбрасываете монеты во всех ячейках (r, c), где r находится в диапазоне от 0 до R включительно. , а c находится в диапазоне от 0 до C включительно. Подбрасывание монеты означает инвертирование значения ячейки от нуля до единицы или от единицы до нуля.

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

Примеры:

1111  
1111  
returns: 1  

01  
01  
returns: 2   

010101011010000101010101  
returns: 20  

000  
000  
001  
011  
returns: 6  

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

Это можно сделать, составив набор, состоящий из всех монет, каждая из которых представлена ​​индексом (т. Е. Если бы всего было 20 монет, этот набор содержал бы 20 элементов, присваивая им индекс от 1 до 20). Затем сделайте все возможные подмножества и посмотрите, какие из них дают ответ (т. е. дает ли нам ход монеты в подмножестве все решки). Наконец, минимизируйте размер хороших комбинаций.

Я не знаю, смог ли я выразить себя слишком ясно... Я опубликую код, если хотите. В любом случае, этот метод слишком трудоемкий и расточительный и невозможен для количества монет> 20 (в моем коде). Как это сделать?


person Arpit Tarang    schedule 27.08.2010    source источник


Ответы (5)


Я думаю, что жадного алгоритма достаточно, с одним шагом на монету.

Каждый ход переворачивает прямоугольное подмножество доски. Некоторые монеты входят в большее количество подмножеств, чем другие: монета в верхнем левом углу (0,0) входит в каждое подмножество, а монета в правом нижнем углу входит только в одно подмножество, а именно в то, которое включает все монеты.

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

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

Повторяйте, пока не закончите.

Вот программа на С++:

#include <iostream>
#include <valarray>
#include <cstdlib>
#include <ctime>
using namespace std;

void print_board( valarray<bool> const &board, size_t cols ) {
    for ( size_t i = 0; i < board.size(); ++ i ) { 
        cout << board[i] << " "; 
        if ( i % cols == cols-1 ) cout << endl;
    }   
    cout << endl;
}

int main() {
    srand( time(NULL) );
    int const rows = 5, cols = 5;

    valarray<bool> board( false, rows * cols );
    for ( size_t i = 0; i < board.size(); ++ i ) board[i] = rand() % 2;
    print_board( board, cols );

    int taken_moves = 0;
    for ( size_t i = board.size(); i > 0; ) { 
        if ( ! board[ -- i ] ) continue;

        size_t sizes[] = { i%cols +1, i/cols +1 }, strides[] = { 1, cols };

        gslice cur_move( 0, valarray<size_t>( sizes, 2 ),
                            valarray<size_t>( strides, 2 ) );
        board[ cur_move ] ^= valarray<bool>( true, sizes[0] * sizes[1] ); 

        cout << sizes[1] << ", " << sizes[0] << endl;
        print_board( board, cols );

        ++ taken_moves;
    }   

    cout << taken_moves << endl;
}
person Potatoswatter    schedule 27.08.2010
comment
Есть ли лучший способ реализовать это? Я просто хотел попробовать valarray, так как я никогда не использовал его раньше, и удивительно, что мне приходится создавать совершенно новый массив единиц для каждого отдельного хода... - person Potatoswatter; 28.08.2010
comment
Вау.. не думал, что проблема может быть уменьшена до такой простой. Большое спасибо!! (Хотя реализация Брайана намного проще для понимания, но это, вероятно, потому, что я не знаю, что такое valarray :P} - person Arpit Tarang; 30.08.2010
comment
@Arpit: Мой также легче понять, потому что это псевдокод. Я даже не утруждаю себя предоставлением реализации flip. - person Brian; 30.08.2010
comment
Да, я понял .. кстати, кто-нибудь понял, как это сделал пользователь 434267? - person Arpit Tarang; 30.08.2010

Не С++. Согласитесь с @Potatoswatter в том, что оптимальное решение является жадным, но мне было интересно, работает ли линейная диофантова система. Эта функция Mathematica делает это:

f[ei_] := (
  xdim = Dimensions[ei][[1]];
  ydim = Dimensions[ei][[2]];

  (* Construct XOR matrixes. These are the base elements representing the
     possible moves *)

  For[i = 1, i < xdim + 1, i++,
   For[j = 1, j < ydim + 1, j++,
    b[i, j] =  Table[If[k <= i && l <= j, -1, 0], {k, 1, xdim}, {l, 1, ydim}]
   ]
  ];

  (*Construct Expected result matrix*)
  Table[rv[i, j] = -1, {i, 1, xdim}, {j, 1, ydim}];

  (*Construct Initial State matrix*)
  Table[eiv[i, j] = ei[[i, j]], {i, 1, xdim}, {j, 1, ydim}];

  (*Now Solve*)
  repl = FindInstance[
           Flatten[Table[(Sum[a[i, j] b[i, j], {i, 1, xdim}, {j, 1, ydim}][[i]][[j]])  
                   eiv[i, j] == rv[i, j], {i, 1, xdim}, {j, 1, ydim}]], 
           Flatten[Table[a[i, j], {i, 1, xdim}, {j, 1, ydim}]]][[1]];

  Table[c[i, j] = a[i, j] /. repl, {i, 1, xdim}, {j, 1, ydim}];

  Print["Result ",xdim ydim-Count[Table[c[i, j], {i, 1, xdim}, {j, 1,ydim}], 0, ydim xdim]];)

При вызове с вашими примерами (-1 вместо 0)

ei = ({
   {1, 1, 1, 1},
   {1, 1, 1, 1}
   });
f[ei];

ei = ({
   {-1, 1},
   {-1, 1}
  });
f[ei];

ei = {{-1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, 1, -1, 
1, -1, 1, -1, 1, -1, 1}};
f[ei];

ei = ({
    {-1, -1, -1},
    {-1, -1, -1},
    {-1, -1, 1},
    {-1, 1, 1}
   });
f[ei];

Результат

Result :1
Result :2
Result :20
Result :6

Or :)

альтернативный текст

Решает случайную задачу 20x20 за 90 секунд на ноутбуке моего бедняги.

person Dr. belisarius    schedule 27.08.2010
comment
То же самое здесь .. не могли бы вы немного объяснить, что он делает? - person Arpit Tarang; 30.08.2010
comment
Мой алгоритм — O(N^2), но задача 20x20 занимает неизмеримое время, а 200x200 — 4 секунды, также на не новом ноутбуке. Есть ли какой-то трюк, который позволяет быстро решать линейные диофантовы уравнения? Википедия, кажется, не дает много информации, но я вижу сопоставление методом грубой силы… - person Potatoswatter; 30.08.2010
comment
@Potatoswatter Нет, это наоборот. Диофантовы уравнения трудно решить, поэтому я хотел попробовать, можно ли это сделать. Как я уже сказал, я считаю (ваше) жадное решение оптимальным. - person Dr. belisarius; 05.09.2010

По сути, вы берете монеты N+M-1 в правой и нижней границах и решаете их, а затем просто рекурсивно вызываете алгоритм для всего остального. Это в основном то, что говорит Potatoswatter. Ниже приведен очень простой рекурсивный алгоритм для него.

Solver(Grid[N][M])
    if Grid[N-1][M-1] == Heads
        Flip(Grid,N-1,M-1)

    for each element i from N-2 to 0 inclusive //This is empty if N is 1
        If Grid[i][M-1] == Heads
            Flip(Grid,i,M-1)

    for each element i from M-2 to 0 inclusive //This is empty if M is 1
        If Grid[N-1][i] == Heads
            Flip(Grid,N-1,i)

    if N>1 and M > 1:
        Solver(Grid.ShallowCopy(N-1, M-1))

    return;     

Примечание. Вероятно, имеет смысл реализовать Grid.ShallowCopy, просто предоставив Solver аргументы для ширины и высоты сетки. Я назвал его Grid.ShallowCopy только для того, чтобы указать, что вы не должны передавать копию сетки, хотя C++ в любом случае не будет делать этого с массивами по умолчанию.

person Brian    schedule 27.08.2010

Простым критерием для переворачивания прямоугольника (x, y) является: ровно когда количество единиц в квадрате 2x2 с верхним левым квадратом (x, y) нечетно.

(код на Питоне)

def flipgame(grid):
  w, h = len(grid[0]), len(grid)
  sol = [[0]*w for y in range(h)]
  for y in range(h-1):
    for x in range(w-1):
      sol[y][x] = grid[y][x] ^ grid[y][x+1] ^ grid[y+1][x] ^ grid[y+1][x+1]
  for y in range(h-1):
    sol[y][w-1] = grid[y][w-1] ^ grid[y+1][w-1]
  for x in range(w-1):
    sol[h-1][x] = grid[h-1][x] ^ grid[h-1][x+1]
  sol[h-1][w-1] = grid[h-1][w-1]
  return sol

Возвращаемый 2D-массив имеет 1 в позиции (x, y), если прямоугольник (x, y) должен быть перевернут, поэтому количество единиц в нем является ответом на ваш исходный вопрос.

РЕДАКТИРОВАТЬ: Чтобы понять, почему это работает: если мы делаем ходы (x,y), (x,y-1), (x-1,y), (x-1,y-1) , инвертируется только квадрат (x,y). Это приводит к приведенному выше коду. Решение должно быть оптимальным, поскольку существует 2^(hw) возможных конфигураций доски и 2^(hw) возможных способов преобразования доски (при условии, что каждый ход можно сделать 0 или 1). раз). Другими словами, существует только одно решение, поэтому приведенное выше решение дает оптимальное решение.

person Jan    schedule 29.08.2010
comment
Я считаю, что это самое быстрое решение... но я просто не понимаю, почему оно работает. Ответьте, пожалуйста... - person Arpit Tarang; 30.08.2010
comment
@Arpit: Во-первых, между проблемами и решениями существует сопоставление 1: 1. Далее, сделав 4 хода, мы можем подбросить одну любую монету. Объединение наборов из четырех ходов позволяет подбрасывать любой набор монет. Однако это приводит к некоторым избыточным ходам. Их легко устранить, проверив, не составляется ли уже набор из 4 для подбрасывания соседней монеты. - person Potatoswatter; 31.08.2010
comment
@Potatoswatter и user434267: Спасибо. - person Arpit Tarang; 31.08.2010

Вы можете использовать рекурсивные испытания.

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

Ваша общая структура алгоритма будет следующей:

const int MAX_FLIPS=10;
const unsigned int TREE_BREADTH=10;

int run_recursion(std::vector<std::vector<bool>> my_grid, int current flips)
{
   bool found = true;
   int temp_val = -1;
   int result = -1;
   //Search for solution with for loops; if true is found in grid, found=false;
   ...
   if  ( ! found && flips < MAX_FLIPS )
   {
       //flip coin.
      for ( unsigned int more_flips=0; more_flips < TREE_BREADTH; more_flips++ )
      {
         //flip one coin
         ...
         //run recursion
         temp_val=run_recursion(my_grid,flips+1)
         if ( (result == -1 && temp_val != -1) || 
              (temp_val != -1 && temp_val < result) )
            result = temp_val;
      }
   }

   return result;
}

... заранее извините за любые опечатки/незначительные синтаксические ошибки. Хотел прототипировать для вас быстрое решение, а не писать полный код...

Или, что еще проще, вы можете просто использовать грубую силу линейных испытаний. Использование внешнего цикла for будет числом испытаний, внутренним циклом for будет переворот в испытании. В каждом цикле вы переворачивали и проверяли, добились ли вы успеха, повторяя свой успех и переворачивая код сверху. Успех приведет к короткому замыканию внутренней петли. В конце внутреннего цикла сохраните результат в массиве. Если сбой после max_moves, сохраните -1. Найдите максимальное значение.

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

Я предлагаю MPI, но CUDA может принести вам очки, так как сейчас жарко.

Надеюсь, это поможет, удачи!

person Jason R. Mick    schedule 27.08.2010
comment
-1: ОП уже пробовал решение грубой силы, и оно не работало достаточно быстро. Я не думаю, что его профессор оценит, если ему скажут, что решение проблемы медленной работы его программы состоит в том, чтобы добавить к ней больше аппаратного обеспечения. Использование MPI или CUDA может произвести впечатление на учителя, но это также намного сложнее, чем просто избежать экспоненциального алгоритма. - person Brian; 27.08.2010