Minesweeper大师来自Google Code Jam(2014)资格赛

时间:2014-04-13 05:29:38

标签: algorithm minesweeper

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

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

Problem Statement:

扫雷(Minesweeper)是一款在20世纪80年代开始流行的计算机游戏,并且仍然包含在某些版本的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网格;输出:

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

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

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

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

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

10 个答案:

答案 0 :(得分:24)

算法

让我们首先定义N,非矿井单元的数量:

N = R * C - M

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

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

那是:

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

只有少数特殊情况需要关注。

单一非矿井

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

单行或单列

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

非地雷太少

如果N为偶数,则必须为&gt; = 4。

如果N为奇数,则必须为&gt; = 9.此外,RC必须为&gt; = 3.

否则没有解决方案。

无法填写前两行

如果N是偶数且您无法用非地雷填充至少两行,请使用N / 2非地雷填充前两行。

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

最后一行中的单一非矿井

如果N % C = 1,请将最后一个非矿区从最后一个完整行移动到下一行。

R=5C=5M=9

的示例
c....
.....
....*
..***
*****

摘要

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

答案 1 :(得分:3)

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

每次点击布局上的位置都会放在a[r-1][c-1]('0'已编入索引)。

对于这个问题,我曾经做过几次错误的尝试,而且每次我都在寻找新的案例。我删除了几个使用goto无法解决问题的情况,并让它跳到最终打印不可能的地方。一个非常简单的解决方案(确实可以说是一个强力解决方案,因为我可以单独编写不同的情况)。对于我的代码,这是editorial。在github上。

答案 2 :(得分:2)

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

基本上算法从充满地雷的电路板开始,并尝试以第一个&#34;点击&#34;的方式移除地雷。会解决董事会。问题是允许点击&#34;为了扩展到另一个小区,扩展将来自另一个必须清除所有其他相邻小区的小区。有时,要扩展到另一个单元,您需要移除其他地雷,最终需要的地雷数量少于所需数量。如果算法到达这样的位置,算法将回溯。

反对可能更简单。从一块空板开始,以一种不会阻止&#34;扩展的方式添加每个矿井。最初的点击。

完整的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)

答案 3 :(得分:2)

我的策略与你的策略非常相似,我通过了小到大。 您是否考虑过下面的案例?

  • R * C - M = 1

  • 只有一行

  • 只有两行


当R&gt;时,我翻转了R和C.下进行。

答案 4 :(得分: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

答案 5 :(得分: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};
        }
    }

答案 6 :(得分:1)

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

  • 对于1x1网格,M必须为零,否则不可能
  • 对于Rx1或1xC网格,我们需要M <= R * C - 2(在最后一个单元格上放置&#39; c&#39;旁边有一个空单元格)
  • 对于RxC网格,我们需要M&lt; = R * C - 4(在角落周围放置3个空单元格的地方&#39; c)

总之,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的社论,我了解我的解决方案有什么问题。我应该涵盖的扫雷的基本规则,非常多。

答案 7 :(得分:1)

前检查

M =(R * C)-1

用所有地雷填充网格,然后点击任意位置。

R == 1 || C == 1

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

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

不可能

算法

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

虽然我们有足够的地雷来填充行或列,当移除时,不会留下单个行或列(while((M >= R1 && C1 > 2) || (M >= C1 && R1 > 2))),我们“修剪”网格(填充地雷并减少{{ 1}}或R1)使用最短边并移除那么多地雷。因此,留下6个地雷的4x5将成为剩余2个地雷的4x4。

  • 如果我们以2 x n网格结束,我们将拥有0个地雷(我们已经完成)或者剩下1个地雷(不可能获胜)。
  • 如果我们以3 x 3网格结束,我们将拥有0个地雷(我们已完成),1个地雷(继续下方)或2个地雷(不可能获胜)。
  • 任何其他组合都是可赢的。我们检查是否C1,如果是这样的话,我们需要从最短的边缘将一个矿井放入一行或一列,然后用剩余的矿井填充最短的边缘。

实施例

我将显示我使用数字进入网格的订单,以帮助进行可视化 M == min(R1,C1)-1

R = 7, C = 6, M = 29

我花了几个不同的尝试来使我的算法正确,但是我用PHP编写了我的算法并得到了小而大的正确。

答案 8 :(得分:0)

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

我认为如果少于(行* cols-4)地雷,它可以解决(3x3矩阵),因为我只需要4个单元用于“c”,其边界为“。”

我的算法如下:

<强>可解

  1. 检查是否有足够的地雷空间(rows*cols - 4 == maximum mines
  2. 例如行== 1,cols == 1;然后是行* cols-2
  3. 条件是否有可能
  4. 构建解决方案

    1. 构建rows*cols matrix,默认值为nil
    2. 转到m[0][0]并指定'c'
    3. 使用m[0][0]
    4. 定义'.'个环境
    5. 从Matrix右下方循环并指定'*',直到地雷结束,然后分配'.'

答案 9 :(得分:0)

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

  

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

     

列举所有可能的案例

     

我们首先检查琐碎的案例:

     

如果只有一个空单元格,那么我们就可以填充所有单元格   除了您单击的单元格以外的地雷。如果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。在这种情况下,我们可以永远   如果空单元的数量更大,则找到有效的矿井配置   这是一种方法:

     

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

     叹息......这是很多案例要涵盖的!我们如何说服自己   当我们编写解决方案时,我们不会错过任何一个角落?

     

蛮力方法

     

对于小输入,电路板尺寸最多为5 x 5.我们可以检查所有   (25选择M)可能的矿井配置并找到一个有效的矿井配置   (即,单击配置中的空单元格会显示所有其他单元格   空细胞)。为了检查矿井配置是否有效,我们可以   运行洪水填充算法(或简单的呼吸优先搜索)   单击空单元格并验证是否可以访问所有其他空单元格   (即,它们在一个连接的组件中)。请注意,我们也应该   检查所有可能的点击位置。这种蛮力方法很快   足够小的输入。

     

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

     

更容易实施的方法

     

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

     

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