硬币翻转游戏:优化问题

时间:2010-08-27 17:41:06

标签: c++ algorithm optimization dynamic

有一个矩形的硬币网格,其中头部由值1表示,尾部由值0表示。您使用2D整数数组表(1到10行/列,包括1和10行)来表示。 / p>

在每次移动中,您选择网格中的任何单个单元格(R,C)(第R行,第C列)并翻转所有单元格中的硬币(r,c),其中r介于0和0之间R,包括,并且c在0和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(在我的代码中)不可能。 如何解决这个问题?

5 个答案:

答案 0 :(得分:9)

我认为贪婪算法就足够了,每枚硬币只需一步。

每一个动作都会翻转棋盘的一个矩形子集。有些硬币包含在更多子集中:左上角(0,0)的硬币位于每个子集中,右下角的硬币只有一个子集,即包含每个硬币的硬币。

因此,选择第一步是显而易见的:如果必须翻转右下角,请翻转每一枚硬币。消除可能的行动。

现在,右下方硬币的左边和右边的邻居只能通过一次剩余移动来翻转。因此,如果必须执行此移动,请执行此操作。邻居的评估顺序无关紧要,因为它们并不是彼此的真正替代品。但是,光栅图案应该足够了。

重复直到完成。

这是一个C ++程序:

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

答案 1 :(得分:3)

不是c ++。同意@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

或者:)

alt text

在我的可怜人的笔记本电脑上,在90秒内解决20x20随机问题。

答案 2 :(得分:2)

基本上,你在右边界和右边界拿着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;     

注意:通过让Solver拥有Grid的宽度和高度的参数来实现Grid.ShallowCopy可能是有意义的。我只调用它Grid.ShallowCopy来表示你不应该传入一个网格副本,尽管C ++默认情况下不会默认使用数组。

答案 3 :(得分:2)

矩形(x,y)被翻转的简单标准似乎是:当左上方(x,y)的2x2平方中的1的数量是奇数时。

(Python中的代码)

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

如果应该翻转矩形(x,y),返回的2D数组的位置(x,y)为1,因此其中1的数量就是原始问题的答案。

编辑:了解其工作原理: 如果我们做移动(x,y),(x,y-1),(x-1,y),(x-1,y-1),则仅反转square(x,y)。这导致了上面的代码。解决方案必须是最优的,因为有2 ^(h w)可能的电路板配置和2 ^(h w)可能的方式来转换电路板(假设每次移动都可以完成0或1次)。换句话说,只有一种解决方案,因此上面产生了最优解。

答案 4 :(得分:-3)

您可以使用递归试验。

您至少需要移动计数并传递矢量的副本。您还需要设置最大移动截止值,以设置搜索树的每个节点出来的分支宽度的限制。请注意,这是一种“暴力”方法。“

您的一般算法结构将是:

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可能会赢得你的褐色点,因为它现在很热。

希望有所帮助,祝你好运!