【问题标题】:Minesweeper master from Google Code Jam(2014) Qualification round来自 Google Code Jam(2014)资格赛的扫雷大师
【发布时间】:2014-04-13 05:29:37
【问题描述】:

这是来自 Google Code Jam 资格赛的问题(现已结束)。如何解决这个问题?

注意:如果您有与答案中讨论的方法不同的方法,请分享它,以便我们扩展我们对解决此问题的不同方法的了解。

Problem Statement:

扫雷是 1980 年代流行的电脑游戏,并且仍然包含在某些版本的 Microsoft Windows 操作系统中。本题思路类似,但不假设你玩过扫雷。

在这个问题中,您正在一个由相同单元格组成的网格上玩游戏。每个单元格的内容最初是隐藏的。在网格的 M 个不同单元格中隐藏着 M 个地雷。没有其他细胞含有地雷。您可以单击任何单元格以显示它。如果显示的单元格包含地雷,则游戏结束,您输了。否则,显示的单元格将包含一个介于 0 和 8 之间的数字,包括 0 和 8,这对应于包含地雷的相邻单元格的数量。如果两个单元共享一个角或一个边,则它们是邻居。此外,如果显示单元格包含 0,则显示单元格的所有邻居也会自动递归显示。当所有不包含地雷的单元格都被揭开时,游戏结束,你赢了。

例如,板的初始配置可能如下所示('*' 表示地雷,'c' 是第一个点击的单元格):

*..*...**.
....*.....
..c..*....
........*.
..........

被点击的单元格附近没有地雷,所以当它被暴露时,它变成0,并且它的8个相邻单元格也被暴露了。这个过程继续,产生以下板:

*..*...**.
1112*.....
00012*....
00001111*.
00000001..

此时,仍有未显示的单元格不包含地雷(用“.”字符表示),因此玩家必须再次单击才能继续游戏。

您想尽快赢得比赛。没有什么比一键获胜更快的了。考虑到棋盘的大小(R x C)和隐藏地雷的数量 M,是否有可能(但不太可能)一键获胜?您可以选择单击的位置。如果可能,请按照输出部分中的说明打印任何有效的矿井配置和单击的坐标。否则,打印“不可能”。

我尝试过的解决方案:

所以对于解决方案,您需要确保每个非矿节点与其他非矿节点在一个 3x3 矩阵中,如果该节点位于网格的边缘,则为 3x2 或 2x2 矩阵;让我们称之为 0Matrix。所以 0Matrix 中的任何节点都有所有非我的邻居。

首先,检查是否需要更少的地雷,或者更少的空节点

if(# mines required < 1/3 of total grid size)
    // Initialize the grid to all clear nodes and populate the mines
    foreach (Node A : the set of non-mine nodes)
        foreach (Node AN : A.neighbors)
            if AN forms a OMatrix with it's neighbors, continue
            else break;
        // If we got here means we can make A a mine since all of it's neighbors 
        // form 0Matricies with their other neighbors
    // End this loop when we've added the number of mines required

else
    // We initialize the grid to all mines and populate the clear nodes
    // Here I handle grids > 3x3; 
    // For smaller grids, I hard coded the logic, eg: 1xn grids, you just populate in 1 dimension

    // Now we know that the # clear nodes required will be 3n+2 or 3n+4
    // eg: if a 4x4 grid need 8 clear nodes : 3(2) + 2

    For (1 -> num 3's needed)
        Add 3 nodes going horizontally
        When horizontal axis is filled, add 3 nodes going vertically
           When vertical axis is filled, go back to horizontal then vertical and so on.

    for(1 -> num 2's needed)
        Add 2 nodes going horizontally or continuing in the direction from above
        When horizontal axis is filled, add 2 nodes going vertically

例如,假设我们有一个需要 8 个干净节点的 4x4 网格,步骤如下:

// Initial grid of all mines
* * * *
* * * *
* * * *
* * * *

// Populating 3's horizontally
. * * *
. * * *
. * * *
* * * *     

. . * *
. . * *
. . * *
* * * *    

// Populating 2's continuing in the same direction as 3's
. . . *
. . . *
. . * *
* * * *        

另一个例子:需要 11 个清晰节点的 4x4 网格;输出:

. . . .
. . . .
. . . *
* * * * 

另一个例子:需要 14 个清晰节点的 4x4 网格;输出:

// Insert the 4 3's horizontally, then switch to vertical to insert the 2's
. . . .
. . . .
. . . .
. . * *  

现在这里我们有一个完全填充的网格,如果我们点击 (0, 0),可以一键解决。

我的解决方案适用于大多数情况,但它没有通过提交(我确实检查了整个 225 个案例的输出文件),所以我猜它有一些问题,我很确定有更好的解决方案.

【问题讨论】:

    标签: algorithm minesweeper


    【解决方案1】:

    算法

    我们先定义N,非矿格数:

    N = R * C - M
    

    一个简单的解决方案是从上到下逐行填充N 非地雷单元格区域。 R=5C=5M=12 的示例:

    c....
    .....
    ...**
    *****
    *****
    

    即:

    • 始终从左上角开始。
    • 从上到下用非地雷填充N / C 行。
    • 从左到右用N % C non-mines 填写下一行。
    • 用地雷填充其余部分。

    您只需要关心一些特殊情况。

    单非矿

    如果N=1,任何配置都是正确的解决方案。

    单行或单列

    如果R=1,只需从左到右填写N非地雷。如果C=1,用(单个)非我的填充N 行。

    非地雷太少

    如果N 是偶数,则它必须 >= 4。

    如果N 是奇数,则它必须 >= 9。此外,RC 必须 >= 3。

    否则没有办法。

    无法填充前两行

    如果N 是偶数,并且您不能用非地雷填充至少两行,则用N / 2 非地雷填充前两行。

    如果N 是奇数,并且您不能用非地雷填充至少两行和用 3 个非地雷填充第三行,那么用(N - 3) / 2 非地雷填充前两行和第三行有 3 个非地雷。

    最后一行单个非我的

    如果N % C = 1,将最后一个非我的从最后一个完整的行移到下一行。

    R=5C=5M=9 的示例:

    c....
    .....
    ....*
    ..***
    *****
    

    总结

    可以编写一个算法来实现这些规则,并在O(1) 中返回对结果雷区的描述。当然,绘制网格需要O(R*C)。我还根据这些想法在 Perl 中编写了一个实现,并被 Code Jam 法官接受。

    【讨论】:

    • 你在 google code jam 上的昵称是什么,所以我可以搜索你的解决方案?这部分不清楚:“如果 N 是偶数并且你不能用非地雷填充至少两行”他怎么会知道?
    • @Leandro:尝试在 5x5 网格中填充 8 个地雷。给定通用算法,第一行有 5 个,第二行有 3 个。但是这样不行,所以必须在第一行填4,在第二行填4。
    • @Leandro "如果 N 是偶数并且你不能用非地雷填充至少两行",可以解释为:如果 n % 2 == 0 和 n / c
    • 当且仅当您使用此策略一键获胜时,您如何证明有可能一键获胜?
    • “非地雷太少”。我注意到N x N 网格和(N*N)-1 地雷(1 个空白空间),当第一次单击该空白单元格时,这也将是自动获胜。但我想你可以说这只是一个“微不足道”的例外。
    【解决方案2】:

    对于这个问题有一个更通用的解决方案,它可以通过小型和大型测试用例。它避免了考虑所有特殊情况,它不关心电路板的尺寸,也不需要任何回溯。

    算法

    基本思路是从一个布满地雷的网格开始,然后从单元格 {0, 0} 开始移除它们,直到棋盘上的地雷数量正确。

    显然需要某种方法来确定接下来要清除哪些地雷以及何时无法清除正确数量的地雷。为此,我们可以保留一个代表董事会的int[][]。每个有地雷的单元格包含-1,没有地雷的单元格包含一个整数,即与单元格相邻的地雷数;和实际游戏中的一样。

    还定义了“边界”的概念,即所有非零的非地雷单元,即与地雷相邻的单元。

    例如配置:

    c . *
    . . *
    . . *
    * * *
    

    将表示为:

     0  2 -1
     0  3 -1
     2  5 -1
    -1 -1 -1
    

    并且边界将包含具有值的单元格:2, 3, 5, 2

    移除地雷时的策略是:

    • 在边界中找到一个与剩余地雷数量相同的单元格。因此,在上面的示例中,如果我们还有 5 个地雷要移除,那么将选择边界上值为 5 的单元格。
    • 未能选择最小的边界单元。所以上面示例中的 2 中的任何一个。
    • 如果所选单元格的值大于剩余的地雷数,则无法构建此板,因此返回 false。
    • 否则移除所选边界单元周围的所有地雷。
    • 重复直到棋盘上出现正确数量的地雷 - 已满足问题的限制条件。

    在 java 中是这样的:

    // Tries to build a board based on the nMines in the test case
    private static boolean buildBoard(TestCase t) throws Exception {
        if (t.nMines >= t.Board.rows() * t.Board.cols()) { 
            return false;
        }
        // Have to remove the cell at 0,0 as the click will go there
        t.Board.removeMine(0, 0);
        while (!t.boardComplete()) {
            Cell c = nextCell(t);
            // If the value of this cell is greater than what we need to remove we can't build a board
            if (t.Board.getCell(c.getRow(), c.getCol()) > t.removalsLeft()) {
                return false;
            }
            t.Board.removeNeighbourMines(c.getRow(), c.getCol());
        }
    
        return true;
    }
    
    // Find frontier cell matching removals left, else pick the smallest valued cell to keep things balanced
    private static Cell nextCell(TestCase t) {
        Cell minCell = null;
        int minVal = Integer.MAX_VALUE;
        for (Cell c : t.Board.getFrontier()) {
            int cellVal = t.Board.getCell(c.getRow(), c.getCol());
            if (cellVal == t.removalsLeft()) {
                return c;
            }
            if (cellVal < minVal) {
                minVal = cellVal;
                minCell = c;
            }
        }
        if (minCell == null) {
            throw new NullPointerException("The min cell wasn't set");
        }
        return minCell;
    }
    

    证明/直觉

    首先,如果可以通过单击解决,则将板定义为有效,即使板上只有一个单元格可以发生此单击。因此,为了使板子有效,所有非我的单元格必须与值为 0 的单元格相邻,如果不是这种情况,则单元格被定义为 unreachable。这是因为我们可以肯定地知道与 0 单元格相邻的所有单元格都不是地雷,因此可以安全地显示它们,并且游戏会自动为玩家执行此操作。

    要证明这个算法的一个关键点是,为了使棋盘保持在有效状态,总是需要清除最小边界单元周围的所有地雷。这很容易证明,只需画出一块板(例如上面的那个)并选择最低值的单元格(在本例中为右上角的 2)。如果只移除了一个地雷,则该板将无效,它将处于以下两种状态之一:

     0  1  1
     0  2 -1
     2  5 -1
    -1 -1 -1
    

     0  1 -1
     0  2  2
     2  5 -1
    -1 -1 -1
    

    它们都有无法访问的单元格。

    所以现在确实总是选择最小的边界单元将使棋盘保持有效状态,而我的第一直觉是继续选择这些单元将遍历所有有效状态,但这是不正确的。这可以通过 4 4 7 之类的测试用例来说明(因此有 9 个非我的单元格)。然后考虑以下板:

     0  2 -1 -1
     2  5 -1 -1
    -1 -1 -1 -1
    -1 -1 -1 -1
    

    继续选择最小的边界单元可能会导致算法执行以下操作:

     0  2 -1 -1
     0  3 -1 -1
     0  3 -1 -1
     0  2 -1 -1
    

    这意味着现在不可能仅移除一个地雷来完成此案例的电路板。然而,选择一个与剩余地雷数量相匹配的边界单元(如果存在的话)可确保选择 5 个,从而产生一个 3x3 的非地雷正方形和测试用例的正确解决方案。

    此时我决定在以下范围内的所有测试用例上尝试该算法:

    0 < R < 20
    0 < C < 20
    0 ≤ M < R * C
    

    并发现它设法正确识别了所有不可能的配置,并为可能的配置构建了看起来明智的解决方案。

    为什么选择与剩余地雷数量(如果存在)具有相同值的边界单元是正确的,进一步的直觉是它允许算法找到需要奇数数的解决方案的配置非地雷。

    最初实现此算法时,我打算编写启发式算法,将非地雷区域构建成方形排列。再次考虑4 4 7 测试用例,它最终会这样做:

     0  0  2 -1
     0  1  4 -1
     2  4 -1 -1
    -1 -1 -1 -1
    

    注意我们现在如何在边界上有一个1,这将确保删除的最后一个单元格完成了给出的正方形:

    c . . *
    . . . *
    . . . *
    * * * *
    

    这意味着启发式会稍微改变为:

    • 选择最小的边界单元
    • 在平局的情况下,选择添加到边界列表的第一个单元格

    这可以通过保持边界单元的 FIFO 队列来实现,但我很快意识到这比最初看起来更棘手。这是因为边界单元的值是相互依赖的,因此需要注意保持边界单元的集合处于正确状态,并且不要在任何类型的哈希值等中使用单元值。我' 确信这是可以做到的,但在意识到只需添加额外的启发式方法来选择任何值等于剩余删除量的单元格时,这似乎是更简单的方法。

    【讨论】:

    • 假设您需要从板上移除 5 个地雷,并且边界由 [1,2,3] 值单元格组成。根据您编写的启发式方法,这将删除 1,然后删除 2,然后失败。您可以通过移除 2 和 3 来移除 5 个地雷。
    • 当从单元格{0,0} 开始时,我想不出这将是整个边界状态的情况。仅给出边界中的值并不能表明任何单元格是否共享地雷。例如,删除 1 可能会将 3 的值减少 1,在这种情况下,剩余的 4 个地雷将被删除以满足条件。如果你能想到一个实际的董事会,这种方法失败了(或者给出了这个边界),我很想知道。我想不出任何这种方法卡住的情况。
    • 你能证明最小的边界总是正确的选择吗?
    • 这就是我要找的!天才!你用来清除地雷的方法效果很好,但你能告诉我们你是如何得出这样的结论的吗?什么样的思考过程让你想到了这个解决方案?谢谢!
    • @MeetM 我已经添加了一些关于我是如何想出这个解决方案的直觉,但真正让我想到的是想出一些不仅仅需要考虑所有问题的东西特殊情况。
    【解决方案3】:

    这是我的code。我解决了不同的情况,例如 number of rows=1number of columns=1number of mines=(r*c)-1 以及其他一些情况。

    每次点击布局上的位置都放在a[r-1][c-1]('0' indexed)。

    对于这个问题,我几乎没有尝试过错误的尝试,而且每次我都在不断寻找新的案例。我消除了一些使用goto 无法解决的情况,并让它跳到打印出不可能的地方。一个非常简单的解决方案(实际上可以说是一个蛮力解决方案,因为我可能分别针对不同的情况进行编码)。这是我的代码的editorial。还有github

    【讨论】:

      【解决方案4】:

      我使用带有回溯的搜索,但我只能解决小的输入。

      基本上,该算法从布满地雷的棋盘开始,并尝试以第一次“点击”解决棋盘的方式移除地雷。问题是,要允许“点击”扩展到另一个单元格,扩展将来自另一个必须清除所有其他相邻单元格的单元格。有时,要扩展到另一个单元,您需要移除其他地雷,最终得到的地雷比需要的少。如果到达这样的位置,算法将回溯。

      也许做相反的事情会更简单。从一个空板开始,并以不会阻止初始点击的“扩展”的方式添加每个地雷。

      完整的 Python 代码如下:

      directions = [
          [-1, -1], [-1, 0], [-1, 1],
          [0, -1],           [0, 1],
          [1,  -1],  [1, 0],  [1, 1],
      ]
      
      def solve(R, C, M):
          def neighbors(i, j):
              for di, dj in directions:
                  if 0 <= (i + di) < R and 0 <= (j + dj) < C:
                      yield (i + di, j + dj)
      
          def neighbors_to_clear(i, j, board):
              return [(ni, nj) for ni, nj in neighbors(i, j) if board[ni][nj] == "*"]
      
          def clear_board(order):
              to_clear = R * C - M - 1
              board = [["*" for _ in range(C)] for _ in range(R)]
              for i, j in order:
                  board[i][j] = "."
                  for ni, nj in neighbors_to_clear(i, j, board):
                      to_clear -= 1
                      board[ni][nj] = "."
              return board, to_clear
      
          def search(ci, cj):
              nodes = []
              board = []
              to_clear = 1
              nodes.append((ci, cj, []))
              while nodes and to_clear > 0:
                  i, j, order = nodes.pop()
                  board, to_clear = clear_board(order)
                  neworder = order + [(i, j)]
                  if to_clear == 0:
                      board[ci][cj] = "c"
                      return board
                  elif to_clear > 0:
                      for ni, nj in neighbors_to_clear(i, j, board):
                          board[ni][nj] = "."
                          nodes.append([ni, nj, neworder])
      
          for i in range(R):
              for j in range(C):
                  board = search(i, j)
                  if board:
                      for row in board:
                          print "".join(row)
                      return
      
          print "Impossible"
          return
      
      T = int(raw_input())
      for i in range(1, T + 1):
          R, C, M = map(int, raw_input().split(" "))
          print("Case #%d:" % i)
          solve(R, C, M)
      

      【讨论】:

        【解决方案5】:

        我的策略和你的非常相似,无论大小我都通过了。 你想过下面的案例吗?

        • R * C - M = 1

        • 只有一行

        • 只有两行


        当 R > C 时,我翻转了 R 和 C。

        【讨论】:

          【解决方案6】:

          我把它分成两个初始的特殊情况,然后有一个通用的算法。 tl;dr 版本是从左上角构建一个正方形的空白区域。与其他答案类似,但特殊情况较少。

          特殊情况

          案例一

          只有 1 个空格。只需点击左上角即可完成。

          案例2

          2 或 3 个空格,网格不是 Rx1 或 1xC。这是不可能的,所以我们很早就失败了。

          算法

          始终单击左上角。从左上角的 2x2 空白方块开始(我们至少有 4 个空白)。现在我们需要添加剩余的空白。然后我们沿着一条边然后另一条边展开正方形,直到我们没有更多的空格。

          消隐顺序示例:

          C  2  6 12
          1  3  7 13
          4  5  8 14
          9 10 11 15
          

          不可能的情况

          请注意,当开始一个新边时,我们必须至少放置两个空格才能使其有效。因此,如果我们只有一个空白要放置,那么这一定是无效的(除非我们的边的长度为 1)。我的逻辑是这样的:

          if maxEdgeLength > 1 and remainingBlanks == 1:
              print('Impossible')
              return
          

          但是,我们可以省略最后一条边的末端,这将给我们两个空白。当然,如果最后一个边的长度超过 2 个空格,我们只能省略最后一个空格!

          我对这种特殊情况的逻辑如下所示:

          if remainingBlanks == 1 and lastEdgeSize > 2:
              mineMatrix[lastBlank] = '*'
              blanks += 1
          

          【讨论】:

            【解决方案7】:

            代码 z 用 cmets 进行自我解释。 O(r+c)

            import java.util.Scanner;
                public class Minesweeper {
                    public static void main(String[] args) {
                        Scanner sc = new Scanner(System.in);
                        int n = sc.nextInt();
                        for(int j=0;j<n;j++) {
                            int r =sc.nextInt(),
                                c = sc.nextInt(),
                                m=sc.nextInt();
                            //handling for only one space.
                            if(r*c-m==1) {
                                System.out.println("Case #"+(int)(j+1)+":");
                                String a[][] = new String[r][c];
                                completeFill(a,r-1,c-1,"*");
                                printAll(a, r-1, c-1);
                            }
                            //handling for 2 rows or cols if num of mines - r*c < 2 not possible.
                            //missed here the handling of one mine.
                            else if(r<2||c<2) {
                                if(((r*c) - m) <2) {
                                    System.out.println("Case #"+(int)(j+1)+":");
                                    System.out.println("Impossible");
                                }
                                else {
                                    System.out.println("Case #"+(int)(j+1)+":");
                                    draw(r,c,m);
                                }
                            }
                            //for all remaining cases r*c - <4 as the click box needs to be zero to propagate
                            else if(((r*c) - m) <4) {
                                System.out.println("Case #"+(int)(j+1)+":");
                                System.out.println("Impossible");
                            }
                            //edge cases found during execution.
                            //row or col =2 and m=1 then not possible.
                            //row==3 and col==3 and m==2 not possible.
                            else {
                                System.out.println("Case #"+(int)(j+1)+":");
                                if(r==3&&m==2&&c==3|| r==2&&m==1 || c==2&&m==1) {
                                    System.out.println("Impossible");
                                }
                                else {
                                    draw(r,c,m);
                                }
                            }
                        }
                    }
                    /*ALGO : IF m < (r and c) then reduce r or c which ever z max 
                     * by two first time then on reduce by factor 1. 
                     * Then give the input to filling (squarefill) function which files max one line 
                     * with given input. and returns the vals of remaining rows and cols.
                     * checking the r,c==2 and r,c==3 edge cases.
                     **/
                    public static void draw(int r,int c, int m) {
                        String a[][] = new String[r][c];
                        int norow=r-1,nocol=c-1;
                        completeFill(a,norow,nocol,".");
                        int startR=0,startC=0;
                        int red = 2;
                        norow = r;
                        nocol = c;
                        int row=r,col=c;
                        boolean first = true;
                        boolean print =true;
                        while(m>0&&norow>0&&nocol>0) {
                            if(m<norow&&m<nocol) {
                                if(norow>nocol) {
                                    norow=norow-red;
                                    //startR = startR + red;
                                }
                                else if(norow<nocol){
                                    nocol=nocol-red;
                                    //startC = startC + red;
                                }
                                else {
                                    if(r>c) {
                                        norow=norow-red;
                                    }
                                    else {
                                        nocol=nocol-red;
                                    }
                                }
                                red=1;
                            }
                            else {
                                int[] temp = squareFill(a, norow, nocol, startR, startC, m,row,col,first);
                                norow = temp[0];
                                nocol = temp[1];
                                startR =r- temp[0];
                                startC =c -temp[1];
                                row = temp[3];
                                col = temp[4];
                                m = temp[2];
                                red=2;
                                //System.out.println(norow + " "+ nocol+ " "+m);
                                if(norow==3&&nocol==3&&m==2 || norow==2&&m==1 || nocol==2&&m==1) {
                                    print =false;
                                    System.out.println("Impossible");
                                    break;
                                }
                            }
                            first = false;
                        }
                        //rectFill(a, 1, r, 1, c);
                        if(print)
                            printAll(a, r-1, c-1);
                    }
                    public static void completeFill(String[][] a,int row,int col,String x) {
                        for(int i=0;i<=row;i++) {
                            for(int j=0;j<=col;j++) {
                                a[i][j] = x;
                            }
                        }
                        a[row][col] = "c";
                    }
                    public static void printAll(String[][] a,int row,int col) {
                        for(int i=0;i<=row;i++) {
                            for(int j=0;j<=col;j++) {
                                System.out.print(a[i][j]);
                            }
                            System.out.println();
                        }
                    }
                    public static int[] squareFill(String[][] a,int norow,int nocol,int startR,int startC,int m,int r, int c, boolean first) {
                        if(norow < nocol) {
                            int fil = 1;
                            m = m - norow;
                            for(int i=startR;i<startR+norow;i++) {
                                for(int j=startC;j<startC+fil;j++) {
                                    a[i][j] = "*";
                                }
                            }
                            nocol= nocol-fil;
                            c = nocol;
                            norow = r;
                        }
                        else {
                            int fil = 1;
                            m = m-nocol;
                            for(int i=startR;i<startR+fil;i++) {
                                for(int j=startC;j<startC+nocol;j++) {
                                    a[i][j] = "*";
                                }
                            }
                            norow = norow-fil;
                            r= norow;
                            nocol = c;
                        }
                        return new int[] {norow,nocol,m,r,c};
                    }
                }
            

            【讨论】:

              【解决方案8】:

              我解决这个问题的方法如下:

              • 对于 1x1 网格,M 必须为零,否则不可能
              • 对于 Rx1 或 1xC 网格,我们需要 M
              • 对于 RxC 网格,我们需要 M

              综上所述,c 无论如何都会在它旁边有非我的单元格,否则是不可能的。这个解决方案对我来说很有意义,我已经根据他们的样本和少量输入检查了输出,但是它没有被接受。

              这是我的代码:

              import sys
              
              fname = sys.argv[1]
              
              handler = open(fname, "r")
              lines = [line.strip() for line in handler]
              
              testcases_count = int(lines.pop(0))
              
              def generate_config(R, C, M):
                  mines = M
              
                  config = []
                  for row in range(1, R+1):
                      if mines >= C:
                          if row >= R - 1:
                              config.append(''.join(['*' * (C - 2), '.' * 2]))
                              mines = mines - C + 2
                          else:
                              config.append(''.join('*' * C))
                              mines = mines - C
                      elif mines > 0:
                          if row == R - 1 and mines >= C - 2:
                              partial_mines = min(mines, C - 2)
                              config.append(''.join(['*' * partial_mines, '.' * (C - partial_mines)]))
                              mines = mines - partial_mines
                          else:
                              config.append(''.join(['*' * mines, '.' * (C - mines)]))
                              mines = 0
                      else:
                          config.append(''.join('.' * C))
              
                  # click the last empty cell
                  config[-1] = ''.join([config[-1][:-1], 'c'])
              
                  return config
              
              for case in range(testcases_count):
                  R, C, M = map(int, lines.pop(0).split(' '))
              
                  # for a 1x1 grid, M has to be zero
                  # for a Rx1 or 1xC grid, we must have M <= # of cells - 2
                  # for others, we need at least 4 empty cells
                  config_possible = (R == 1 and C == 1 and M==0) or ((R == 1 or C == 1) and M <= R * C - 2) or (R > 1 and C > 1 and M <= R * C - 4)
              
                  config = generate_config(R, C, M) if config_possible else None
              
                  print "Case #%d:" % (case+1)
                  if config:
                      for line in config: print line
                  else:
                      print "Impossible"
              
              handler.close()
              

              与他们在网站上的样本和他们提供的少量输入相比,它工作得很好,但看起来我错过了一些东西。

              这是样本的输出:

              Case #1:
              Impossible
              Case #2:
              *
              .
              c
              Case #3:
              Impossible
              Case #4:
              ***....
              .......
              .......
              ......c
              Case #5:
              **********
              **********
              **********
              **********
              **********
              **********
              **********
              **********
              **........
              .........c
              

              更新:阅读 vinaykumar 的社论,我明白我的解决方案有什么问题。扫雷的基本规则,我应该已经讲过了。

              【讨论】:

                【解决方案9】:

                预检查

                M = (R * C) - 1

                用所有地雷填充网格并单击任意位置。

                R == 1 || C == 1

                按左/右(或上/下)顺序填写:点击、非地雷、地雷(例如c...****)。

                M == (R * C) - 2 || M == (R * C) - 3

                不可能

                算法

                我从一个“空”网格开始(全部为.s)并将点击放在一个角落(我将使用左上角进行点击,并从右下角开始填充地雷)。
                我们将使用R1C1 作为我们的“当前”行和列。

                虽然我们有足够的地雷来填充一行或一列,但移除后不会留下任何一行或一列 (while((M &gt;= R1 &amp;&amp; C1 &gt; 2) || (M &gt;= C1 &amp;&amp; R1 &gt; 2))),我们会“修剪”网格(填充地雷并减少 R1C1) 使用最短的边并移除那么多地雷。因此,剩下 6 个地雷的 4x5 将变成剩下 2 个地雷的 4x4。

                • 如果我们最终得到 2 x n 网格,我们将要么有 0 个地雷(我们已经完成),要么剩下 1 个地雷(不可能获胜)。
                • 如果我们最终得到一个 3 x 3 的网格,我们将要么有 0 个地雷(我们完成了)、1 个地雷(在下面继续),要么有 2 个地雷(不可能获胜)。
                • 任何其他组合都可以获胜。我们检查是否M == min(R1,C1)-1,如果是,我们需要在最短边的一行或一列中放置一个地雷,然后用剩余的地雷填充最短边。

                示例

                我将用数字显示我在网格中输入地雷的顺序,只是为了帮助可视化
                R = 7, C = 6, M = 29

                c...42
                ....42
                ...742
                ..6742
                555542
                333332
                111111  
                

                我尝试了几次不同的尝试才使我的算法正确,但我用 PHP 编写了我的算法并且大小都正确。

                【讨论】:

                  【解决方案10】:

                  我也在这个问题上试试运气,但由于某种原因没有通过检查。

                  我认为,如果地雷少于 (rows*cols-4),则对于 (3x3 矩阵) 是可解的,因为“c”只需要 4 个单元格,其边界为“.”。

                  我的算法如下:

                  可以解决吗?

                  1. 检查是否有足够的空间放置地雷 (rows*cols - 4 == maximum mines)
                  2. 行 == 1、列 == 1 等例外情况;然后是 rows*cols-2
                  3. 可能或不可能的条件

                  构建解决方案

                  1. 构建rows*cols matrix,默认值为nil
                  2. 转到m[0][0] 并分配'c'
                  3. '.' 定义m[0][0] 环境
                  4. 从矩阵的右下角循环并分配'*'直到地雷结束,然后分配'.'

                  【讨论】:

                  • 根据您的回答,我怀疑至少存在一种情况,即只有一个非地雷方块,您将其标记为不可能,而不是一个充满地雷的网格,只有一个“c”。
                  • 你的算法给出了不可能作为案例 1 5 4 的解决方案。但输出应该是 ****c
                  • 对于案例 4 4 7 它也失败了。您的解决方案是 c... .... .*** **** 但是当您实际单击该位置 'c' 时,它返回 0000 1232 .*** **** 所以游戏还没有完成,你没有一步获胜。
                  • 我提交了类似的解决方案。但是我错过了极端情况。
                  【解决方案11】:

                  可以在here找到解决方案。以下页面内容。

                  有很多方法可以生成有效的矿山配置。在这个 分析,我们尝试枚举所有可能的情况,并尝试生成一个 每个案例的有效配置(如果存在)。后来,有了 一些见解,我们提供了一个更容易实现的算法来生成 有效的矿山配置(如果存在)。

                  列举所有可能的情况

                  我们从检查琐碎的案例开始:

                  如果只有一个空单元格,那么我们可以用 除了您单击的单元格之外的地雷。如果 R = 1 或 C = 1,地雷 可以分别从左到右或从上到下放置, 分别单击最右侧或最底部的单元格。如果 board 不在上面的两个微不足道的情况下,这意味着 board 有 至少 2 x 2 大小。然后,我们可以手动检查:

                  如果空单元格的数量为 2 或 3,则不可能有 有效的配置。如果 R = 2 或 C = 2,则存在有效配置 仅当 M 为偶数时。例如,如果 R = 2、C = 7 和 M = 5,则为 不可能,因为 M 是奇数。但是,如果 M = 6,我们可以放置地雷 在板的左侧,然后单击右下角,例如 这: *.... *...c 如果板子不属于上述任何一种情况,则意味着板子至少为 3 x 3 尺寸。在这种情况下,我们总是可以 如果空单元的数量更大,则找到有效的矿山配置 大于 9。这是一种方法:

                  如果空单元格的数量等于或大于 3 * C,则 地雷可以从上到下逐行放置。如果 剩余地雷的数量可以完全填满该行或小于 C - 2 然后将地雷从左到右放置在该行中。否则,剩余地雷的数量正好是 C - 1,将最后一个地雷放入 下一行。例如: ************ *****。 ****.. ...... -> *..... ………… .....c .....c 如果空单元格的数量小于 3 * C 但至少为 9,我们首先用地雷填充所有行,除了 最后 3 行。对于最后 3 行,我们填充剩余的地雷 从最左边的列开始逐列。如果剩余的地雷 最后一列是两个,那么最后一列必须放在下一列。 例如: ************ .... -> *... **.... *....... *....c *..c 现在,我们最多剩下 9 个空单元格,它们位于右下角的 3 x 3 方形单元格中 角落。在这种情况下,我们可以手动检查是否为空 细胞是 5 或 7,不可能有一个有效的地雷配置。 否则,我们可以硬编码每个数量的有效配置 3 x 3 方形单元格中的空单元格。

                  叹息...要涵盖的案例太多了!我们如何说服自己 当我们编写解决方案时,我们不会错过任何极端情况?

                  蛮力方法

                  对于小输入,板子大小最多为 5 x 5。我们可以检查所有 (25 选择 M) 可能的矿井配置并找到一个有效的 (即,单击配置中的空单元格会显示所有其他 空单元格)。要检查矿井配置是否有效,我们可以 从 单击空单元格并验证是否可以访问所有其他空单元格 (即,它们在一个连接的组件中)。请注意,我们还应该 检查所有可能的点击位置。这种蛮力方法很快 足够小的输入。

                  蛮力方法可用于检查(对于较小的 R 值, C, M) 我们的枚举策略是否存在假阴性 多于。存在有效地雷时发现假阴性 配置,但上面的枚举策略产生了不可能。 一旦我们确信我们的枚举策略不会产生 任何假阴性,我们都可以用它来解决大输入。

                  更容易实施的方法

                  在使用了几个有效的地雷配置后,使用 上面的枚举策略,你可能会注意到一个模式:在一个有效的矿井中 配置,特定行中的地雷数量始终相等 或大于其下方行的地雷数和所有地雷数 地雷连续左对齐。有了这个洞察力,我们可以实现一个 更简单的回溯算法,从顶部逐行放置地雷 当我们继续填写时,地雷数量不会增加 如果当前行的配置是下一行并修剪 无效(可以通过单击右下角的单元格来检查)。这 带修剪的回溯可以处理多达 50 x 50 大小的板 合理的时间并且更易于实施(即,无需 列举角落/棘手的案例)。

                  如果比赛时间更短,我们可能没有足够的时间 列举所有可能的情况。在这种情况下,投注 回溯算法(或任何其他更容易 实施)可能是一个好主意。找到这样的算法是一门艺术:)。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-06-10
                    • 2014-08-27
                    相关资源
                    最近更新 更多