我们先来定义一下 N ,非矿井单元的数量:
N
N = R * C - M
一个简单的解决方案就是填补一个区域 N 非矿井单元从上到下逐行。示例 R=5 , C=5 , M=12 :
R=5
C=5
M=12
c.... ..... ...** ***** *****
那是:
N / C
N % C
只有少数特殊情况你需要关心。
如果 N=1 ,任何配置都是正确的解决方案。
N=1
如果 R=1 ,只需填写 N 从左到右的非地雷。如果 C=1 , 填 N 带有(单个)非矿井的行。
R=1
C=1
如果 N 是偶数,它必须是> = 4。
如果 N 是奇数,它必须是> = 9.此外, R 和 C 必须> = 3。
R
C
否则没有解决方案。
如果 N 是偶数,你不能用非地雷填充至少两行,然后填充前两行 N / 2 非矿。
N / 2
如果 N 很奇怪,你不能用非地雷填充至少两行,用3个非地雷填充第三行,然后填充前两行 (N - 3) / 2 非地雷和第三排有3个非地雷。
(N - 3) / 2
如果 N % C = 1 ,将最终的非矿井从最后一个完整行移动到下一行。
N % C = 1
示例 R=5 , C=5 , M=9 :
M=9
c.... ..... ....* ..*** *****
可以编写一个实现这些规则的算法,并返回生成的矿区的描述 O(1) 。绘制网格需要 O(R*C) , 当然。我还根据Code Jam Judge接受的这些想法在Perl中编写了一个实现。
O(1)
O(R*C)
代码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}; } }
用所有地雷填充网格并在任何地方点击。
按顺序向左/向右(或向上/向下)填充:点击,非地雷,地雷(例如 c...**** )。
c...****
不可能
我从一个“空”网格开始(全部 . s)并将咔嗒声放在角落里(我将使用左上角进行点击,然后从右下角开始填充地雷)。 我们会用的 R1 和 C1 作为我们的“当前”行和列。
.
R1
C1
虽然我们有足够的地雷来填充行或列,但在删除时,不会留下一行或一列( while((M >= R1 && C1 > 2) || (M >= C1 && R1 > 2)) ),我们“修剪”网格(填充地雷并减少 R1 要么 C1 )使用最短的一面并移除那么多的地雷。因此,留下6个地雷的4x5将成为剩余2个地雷的4x4。
while((M >= R1 && C1 > 2) || (M >= C1 && R1 > 2))
M == min(R1,C1)-1
我将使用数字显示我使用数字进入网格的顺序,以帮助进行可视化 R = 7, C = 6, M = 29
R = 7, C = 6, M = 29
c...42 ....42 ...742 ..6742 555542 333332 111111
我花了几个不同的尝试来使我的算法正确,但我用PHP编写了我的算法并得到了小和大的正确。
我也试过这个问题,但由于某种原因没有通过检查。
我认为,如果少于(行* cols-4)地雷,它可以解决(3x3矩阵),因为我只需要4个单元用于“c”,其边界为“。”。
我的算法如下:
的 可解? 强> :
rows*cols - 4 == maximum mines
的 构建解决方案 强>
rows*cols matrix
m[0][0]
'c'
'.'
'*'
我使用回溯搜索,但我只能解决小输入问题。
基本上算法从充满了地雷的电路板开始,并尝试以第一次“点击”解决电路板的方式移除地雷。问题是,为了允许“点击”扩展到另一个单元格,扩展将来自另一个必须清除所有其他相邻单元格的单元格。有时,要扩展到另一个单元,您需要移除其他地雷,最终需要的地雷数量少于所需数量。如果算法到达这样的位置,算法将回溯。
也许相反的做法更简单。从空板开始,以一种不会阻止初始点击“扩展”的方式添加每个矿井。
完整的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)
可以找到解决方案 这里 。以下页面的内容。
有许多方法可以生成有效的矿井配置。在这 分析,我们尝试枚举所有可能的情况并尝试生成一个 每种情况的有效配置(如果存在)。之后,有了 一些见解,我们提供了一种更容易实现的算法来生成 有效的矿井配置(如果存在)。 列举所有可能的案例 我们首先检查一些琐碎的案例: 如果只有一个空单元格,那么我们就可以填充所有单元格 除了您单击的单元格以外的地雷。如果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大小的板 合理的时间并且更容易实施(即,不需要 列举角落/棘手案例)。 如果比赛时间较短,我们可能没有足够的时间 枚举所有可能的情况。在这种情况下,投注 回溯算法(或任何其他更容易的算法) 实施)可能是一个好主意。找到这样的算法是一门艺术:)。
有许多方法可以生成有效的矿井配置。在这 分析,我们尝试枚举所有可能的情况并尝试生成一个 每种情况的有效配置(如果存在)。之后,有了 一些见解,我们提供了一种更容易实现的算法来生成 有效的矿井配置(如果存在)。
列举所有可能的案例
我们首先检查一些琐碎的案例:
如果只有一个空单元格,那么我们就可以填充所有单元格 除了您单击的单元格以外的地雷。如果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大小的板 合理的时间并且更容易实施(即,不需要 列举角落/棘手案例)。
如果比赛时间较短,我们可能没有足够的时间 枚举所有可能的情况。在这种情况下,投注 回溯算法(或任何其他更容易的算法) 实施)可能是一个好主意。找到这样的算法是一门艺术:)。
我解决这个问题的方法如下:
综上所述, 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的社论,我理解我的解决方案有什么问题。扫雷的基本规则,我应该涵盖,几乎。
我把它分成两个初始特殊情况,然后有一个通用算法。 tl; dr版本是从左上角构建一个正方形的空格。与其他答案类似,但特殊情况较少。
只有1个空格。只需单击左上角即可完成。
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