30

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

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

问题陈述:

Minesweeper 是一种在 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 个案例的输出文件),所以我猜它有一些问题,我很确定有更好的解决方案。

4

11 回答 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。此外,R并且C必须 >= 3。

否则没有解决办法。

无法填充前两行

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

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

最后一行单非矿

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

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

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

概括

可以编写一个算法来实现这些规则,并在O(1). 当然,绘制网格需要O(R*C)。我还根据 Code Jam Judge 接受的这些想法在 Perl 中编写了一个实现。

于 2014-04-13T12:24:23.950 回答
15

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

算法

基本思想是从一个充满地雷的网格开始,然后从单元格 {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 队列来实现,但我很快意识到这比最初看起来更棘手。这是因为边界单元的值是相互依赖的,因此需要注意保持边界单元的集合处于正确状态,并且不要在任何类型的哈希值等中使用单元值。我' 确信这是可以做到的,但在意识到只需添加额外的启发式方法来选择任何值等于剩余删除量的单元格时,这似乎是更简单的方法。

于 2014-04-15T10:36:43.510 回答
3

这是我的代码。我解决了不同的案例,比如 ifnumber of rows=1number of columns=1ifnumber of mines=(r*c)-1以及其他几个案例。

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

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

于 2014-04-13T12:33:35.137 回答
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)
于 2014-04-13T11:12:23.623 回答
2

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

  • R * C - M = 1

  • 只有一排

  • 只有两行


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

于 2014-04-13T11:37:44.323 回答
2

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

特别案例

情况1

只有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
于 2014-04-13T18:54:18.840 回答
1

代码 z 带有注释的自我解释。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};
        }
    }
于 2014-04-14T03:55:38.463 回答
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 的社论,我明白我的解决方案有什么问题。我应该涵盖的扫雷游戏的基本规则,差不多。

于 2014-04-14T10:30:18.637 回答
1

预检

M = (R * C) - 1

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

R == 1 || C == 1

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

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

不可能的

算法

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

虽然我们有足够的地雷来填充一行或一列,但移除时不会留下一行或一列while((M >= R1 && C1 > 2) || (M >= C1 && R1 > 2))(地雷。因此,剩下 6 个地雷的 4x5 将变成剩下 2 个地雷的 4x4。 R1C1

  • 如果我们最终得到 2 xn 网格,我们将要么有 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 编写了我的算法,并且大小都正确。

于 2014-04-20T06:44:47.107 回答
0

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

我认为如果有少于 (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. 从矩阵的右下角循环并分配'*',直到地雷结束,然后分配'.'
于 2014-04-13T06:17:53.513 回答
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)可能的矿井配置并找到一个有效的(即,单击配置中的空单元格会显示所有其他空单元格)。要检查矿井配置是否有效,我们可以从单击的空单元格运行洪水填充算法(或简单的呼吸优先搜索)并验证所有其他空单元格是否可到达(即,它们在一个连接组件中) . 请注意,我们还应该检查所有可能的点击位置。这种蛮力方法对于小输入来说足够快。

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

更容易实施的方法

在使用上面的枚举策略玩弄了几个有效的地雷配置之后,您可能会注意到一个模式:在一个有效的地雷配置中,特定行中的地雷数总是等于或大于其下方行的地雷数,并且所有的地雷都是左对齐的。有了这个见解,我们可以实现一个更简单的回溯算法,该算法从上到下逐行放置地雷,当我们继续填充下一行时,如果当前行的配置无效(它可以通过单击右下角的单元格进行检查)。这种带有修剪的回溯可以在合理的时间内处理多达 50 x 50 大小的电路板,并且实现起来更简单(即,无需列举角落/棘手的情况)。

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

于 2015-08-03T01:51:58.223 回答