谷歌编程大赛(2014)资格赛中的扫雷高手

30

这是谷歌编程大赛的一道问题(现已结束)。如何解决此问题?

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

问题陈述:

扫雷是一种计算机游戏,在20世纪80年代变得流行,并仍然包含在某些版本的Microsoft Windows操作系统中。这个问题有一个类似的想法,但它并不假设你玩过扫雷。

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

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

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

点击的方格周围没有地雷,因此当它被揭开时,它会变成0,并且它周围8个方格也将被揭开。这个过程继续进行,导致出现以下棋盘:

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

目前,仍然存在未揭示的不含地雷的单元格(用'.'字符表示),因此玩家必须再次点击以继续游戏。

您希望尽快赢得比赛。没有比一次点击更快的方式了。考虑到棋盘的大小(R x C) 和隐藏的地雷数量M,是否可能(虽然不太可能)在一次点击中获胜?您可以选择点击的位置。如果可能,请按照输出部分的规定打印出任何有效的地雷配置和您的点击坐标。否则,请打印“Impossible”。

我的尝试解决方案:

因此,对于解决方案,您需要确保每个非地雷节点都与其他非地雷节点处于3x3矩阵中,或者是3x2或2x2矩阵,如果节点位于网格的边缘,则为0矩阵。因此,任何在零矩阵中的节点都具有所有非地雷邻居。

首先,检查需要的地雷数量或空节点数量是否较少。

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
例如,假设我们有一个4x4的网格,需要8个干净节点,下面是步骤:
// 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个案例的输出文件),所以我猜它存在一些问题,并且我相信有更好的解决方案。

11个回答

24

算法

首先定义 N,表示非地雷方块的数量:

N = R * C - M

一个简单的解决方案是按照自上而下的顺序,逐行填充一个由N个非地雷单元格组成的区域。以 R=5, C=5, M=12 为例:

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

即:

  • 始终从左上角开始。
  • 用非地雷填充从上到下的N/C行。
  • 用从左到右的N%C个非地雷填充下一行。
  • 用地雷填充其余位置。

你只需要关心几种特殊情况。

单个非地雷

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

单行或单列

如果R=1,则只需从左到右填入N个非地雷。如果C=1,则使用一个非地雷填充N行。

非地雷数量太少

如果N为偶数,则必须大于等于4。

如果N为奇数,则必须大于等于9。此外,RC必须大于等于3。

否则就没有解。

无法填充前两行

如果N为偶数且无法用至少两行非地雷填充,则用N/2个非地雷填充前两行。

如果N为奇数且无法用至少两行非地雷和第三行三个非地雷填充,则用(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
1
@Leandro:尝试在一个5x5的网格中填充8个地雷。根据通用算法,你会在第一行填5个,在第二行填3个。然而,这并不起作用,所以你必须在第一行填4个,在第二行填4个。 - gengkev
1
@Leandro "如果N是偶数且你无法用非地雷填充至少两行",可以解释为:如果 n % 2 == 0 并且 n / c < 2: 做某事 - Kristiono Setyadi
如何证明只有采用这种策略才能在一次点击中赢得比赛? - Richard Fung
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Kevin Fegan

15
这个问题有一个更通用的解决方案,可以通过小和大的测试案例。它避免了考虑所有特殊情况,不关心棋盘的尺寸,并且不需要任何回溯。
算法的基本思想是从包含地雷的网格开始,并从单元格{0,0}开始删除它们,直到棋盘上有正确数量的地雷。
显然,需要某种方法来确定下一个要删除的地雷以及何时无法删除正确数量的地雷。为此,我们可以保留一个int [][]表示棋盘。每个包含地雷的单元格都包含-1,而那些没有地雷的单元格包含一个整数,该整数是相邻单元格中地雷的数量,与实际游戏中相同。
还要定义“frontier”的概念,即所有非矿物且非零的单元格,即具有相邻矿物的单元格。
例如,配置:
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;
}

PROOF / INTUITION

首先,如果棋盘上只有一个单元格可以单击解决,则将其定义为有效。因此,为了使棋盘有效,所有非地雷单元格必须与值为0的单元格相邻,如果不是这种情况,则该单元格被定义为无法到达。这是因为我们确信,与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个地雷。 - GDanger
3
你能证明最小边界始终是正确的选择吗? - Thomas Ahle
1
这正是我在寻找的!太聪明了!你用来消除地雷的方法非常有效,但你能告诉我们你是如何得出这个结论的吗?是什么思维过程导致了你采用这个解决方案?谢谢! - MeetM
@ThomasAhle 我已经添加了一些关于为什么最小前沿单元格是正确的信息,但请注意,该算法并不总是选择最小单元格。 - Choc13
2
@MeetM 我添加了一些与我想出这个解决方案的直觉,但真正引导我想到它的是希望有所创新,不只需要考虑所有特殊情况。 - Choc13
显示剩余2条评论

3
这是我的代码。我解决了不同的情况,例如如果行数=1列数=1或者如果地雷数量=(r*c)-1和其他一些情况。
每次单击布局上的位置都放置在a[r-1][c-1](从0开始计数)。
对于这个问题,我尝试过几次错误的尝试,并且每次我都找到了一个新的情况。我使用goto消除了几种无法解决的情况,并让它跳转到结束位置,打印出无法解决。这是一个非常简单的解决方案(实际上可以说是一种暴力解决方案,因为我逐个编码了可能出现的不同情况)。这是我的代码的一个编辑。以及在github上。

2
我使用了回溯搜索,但只能解决小型输入。
基本上,该算法从充满地雷的棋盘开始尝试以一种方式移除地雷,使得第一个“点击”就可以解决整个棋盘。问题在于,为了让“点击”扩展到另一个单元格,扩展必须来自另一个单元格,该单元格必须清除所有其他相邻单元格。有时,要扩展到另一个单元格,您需要移除其他地雷,并最终获得少于所需数量的地雷。如果算法达到这样的位置,它将进行回溯。
也许做相反的事情更简单。从空棋盘开始,以不会阻止初始点击“扩展”的方式添加每个地雷。
完整的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)

2
我将这个问题分成了两个初始特殊情况,然后有了一个通用算法。简而言之,就是从左上角建立一个空格方块。与其他答案类似,但特殊情况较少。
特殊情况
情况1:只有一个空格。只需单击左上角即可完成。
情况2:有2或3个空格,并且网格不是Rx1或1xC。这是不可能的,所以我们很早就失败了。
算法
始终在左上角单击。从左上角开始,留下一个2x2的空白方块(我们至少有4个空白)。现在我们需要添加剩余的空格。然后我们沿着一条边扩展正方形,然后沿着另一条边扩展,直到没有更多的空格为止。
空格顺序示例:
C  2  6 12
1  3  7 13
4  5  8 14
9 10 11 15

无法完成的情况

请注意,当开始一条新边时,我们必须至少留出两个空格才能使其有效。因此,如果我们只有一个空格可以使用,则必须无效(除非我们的边长为一)。我的逻辑如下:

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

然而,我们可以省略掉最后一条边的末尾,这样现在就有两个空白了。当然,只有当最后一条边的长度超过2个空格时,我们才能省略掉最后一个空格!

对于这种特殊情况,我的逻辑如下:

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

2

我的策略与你的非常相似,我通过了小规模和大规模的测试。

您考虑过以下情况吗?

  • R * C - M = 1

  • 只有一行

  • 只有两行


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


1

代码很清晰,并带有注释。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};
        }
    }

1
我的解决问题的方法如下:
  • 对于1x1网格,M必须为零,否则不可能
  • 对于Rx1或1xC网格,我们需要M <= R * C - 2(在最后一个有空单元格的单元格上放置'c')
  • 对于RxC网格,我们需要M <= R * C - 4(将'c'放在拥有3个空单元格周围的角落上)

总之,无论如何,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的编辑意见后,我明白了我的解决方案存在的问题。我应该涵盖扫雷的基本规则。


1

预检查

M = (R * C) - 1

将网格填满所有的地雷,然后在任意位置点击。

R == 1 || C == 1

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

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

不可能

算法

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

当我们有足够的地雷来填满一行或一列时,删除后不会留下单独的一行或一列(while((M >= R1 && C1 > 2) || (M >= C1 && R1 > 2))),我们就会使用最短的边进行"修剪"(填充地雷并减少R1C1),然后移除相应数量的地雷。因此,一个有6个地雷的4x5方格将变成一个有2个地雷的4x4方格。

  • 如果我们最终得到了一个2 x n的网格,我们要么没有地雷(完成),要么只剩下1枚地雷(无法获胜)。
  • 如果我们最终得到了一个3 x 3的网格,我们要么没有地雷(完成),要么只剩下1枚地雷(继续下面的步骤),要么剩下2枚地雷(无法获胜)。
  • 任何其他组合都是可赢的。我们检查M == min(R1,C1)-1,如果是这样,我们将需要在最短边的内侧放置一枚地雷,然后用剩余的地雷填充最短边。

示例

我将展示我输入矿井的顺序,并附上数字以帮助可视化
R = 7, C = 6, M = 29

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

我尝试了几次才正确地编写出算法,但我用PHP编写了我的算法,并且小规模和大规模都正确。


0

解决方案可以在这里找到。以下是页面内容。

有许多方法可以生成有效的扫雷配置。在这个分析中,我们尝试枚举所有可能的情况,并尝试为每种情况生成有效的配置(如果存在)。稍后,在获得一些见解之后,我们提供了一个更容易实现的算法来生成有效的扫雷配置(如果存在)。
枚举所有可能的情况
我们从检查简单的情况开始:
如果只有一个空单元格,那么我们可以将所有单元格都填满地雷,除了您单击的单元格。如果R = 1或C = 1,则地雷可以从左到右或从上到下放置,然后单击最右边或最下面的单元格。如果板子不在上述两种简单情况中,那么它意味着板子至少具有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)可能的扫雷配置,并找到一个有效的配置(即,单击配置中的空单元格会显示所有其他空单元格)。要检查扫雷配置是否有效,我们可以从单击的空单元格运行洪泛算法(或简单的广度优先搜索),并验证所有其他空单元格是否可达(即,它们在一个连通组件中)。请注意,我们还应检查所有可能的单击位置。这种暴力方法对于小输入

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接