【问题标题】:Coin flipping game: Optimization problem抛硬币游戏:优化问题
【发布时间】:2011-04-04 22:44:38
【问题描述】:

有一个长方形的硬币网格,正面由值 1 表示,反面由值 0 表示。您可以使用 2D 整数数组表(1 到 10 行/列,包括 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)。然后制作所有可能的子集,看看它们中的哪一个给出答案(即,如果在子集中的硬币上移动给了我们所有的反面)。最后,最小化好的组合的大小。

我不知道我是否能够太清楚地表达自己......如果你愿意,我会发布一个代码。 无论如何,这种方法太耗时和浪费,并且对于 no.of coin>20(在我的代码中)是不可能的。 这个怎么办?

【问题讨论】:

    标签: c++ algorithm optimization dynamic


    【解决方案1】:

    我认为贪心算法就足够了,每个硬币一步。

    每一步都会翻转棋盘的一个矩形子集。有些硬币比其他硬币包含在更多的子集中:左上角 (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;
    }
    

    【讨论】:

    • 有没有更好的方法来实现这个?我只是想尝试valarray,因为我以前从未使用过它,令人惊讶的是,我必须为每一步构建一个全新的 1 数组……
    • 哇.. 没想到问题可以这么简单。非常感谢!! (虽然 Brian 的实现更容易理解,但这可能是因为我不知道 valarray 是什么:P}
    • @Arpit:我的也更容易理解,因为它是伪代码。我什至懒得提供翻转的实现。
    • 是的,我明白了.. 顺便说一句,any1 知道 user434267 是如何做到的吗?
    【解决方案2】:

    不是 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
    

    或者:)

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

    【讨论】:

    • 这里也一样..你能解释一下它的作用吗?
    • 我的算法是 O(N^2),但是 20x20 的问题需要不可估量的时间,而 200x200 需要 4 秒,在不是新的笔记本电脑上也是如此。是否有某种技巧可以使线性丢番图方程快速求解?维基百科似乎没有提供太多见解,但我可以看到蛮力映射......
    • @Potatoswatter Nop,这是另一种方式。丢番图方程很难解决,这就是为什么我想尝试是否可以解决。正如我所说,我认为(你的)贪婪解决方案是最佳的。
    【解决方案3】:

    基本上,您将在右侧和底部边框中取出 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++ 不会对数组执行此操作。

    【讨论】:

      【解决方案4】:

      矩形(x,y) 被翻转的一个简单标准似乎是:恰好当左上角正方形 (x,y) 的 2x2 正方形中的个数为奇数时。

      (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),则返回的二维数组的位置 (x,y) 为 1,因此其中的个数就是您原始问题的答案。

      编辑: 要了解它的工作原理: 如果我们移动 (x,y), (x,y-1), (x-1,y), (x-1,y-1),只有正方形 (x,y) 被反转。这导致了上面的代码。解决方案必须是最优的,因为有 2^(hw) 种可能的棋盘配置和 2^(hw) 种可能的方式来变换棋盘(假设每一步都可以完成 0 或 1次)。换句话说,只有一种解决方案,因此上面产生了最优的解决方案。

      【讨论】:

      • 我相信这是迄今为止最快的解决方案......但我只是不明白它为什么有效。请回复...
      • @Arpit:首先,问题和解决方案之间存在 1:1 的映射关系。接下来,通过 4 步,我们可以掷出任何一枚硬币。结合一组四个动作可以翻转任何一组硬币。但是,这会导致一些多余的动作。通过检查是否已经制作了一组 4 组来翻转相邻的硬币,很容易消除这些问题。
      • @Potatoswatter 和 user434267:谢谢。
      【解决方案5】:

      您可以使用递归试验。

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

      你的一般算法结构是:

      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 可能会赢得你的布朗尼积分,因为它现在很热。

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

      【讨论】:

      • -1:OP 已经在尝试蛮力解决方案,但运行速度不够快。我不认为他的教授会喜欢被告知解决他的程序慢的方法是投入更多的硬件。使用 MPI 或 CUDA 可能会成功地给老师留下深刻印象,但它也比仅仅避免使用指数算法要困难得多。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-23
      • 2021-03-12
      • 2021-08-30
      相关资源
      最近更新 更多