这是来自Google Code Jam资格回合的问题(现已结束)。如何解决这个问题?
注意:如果您的方法与答案中讨论的方法不同,请分享,以便我们扩展我们对解决此问题的不同方法的了解。
扫雷(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个案例输出文件),所以我猜它有一些问题,我很确定有更好的解决方案
答案 0 :(得分: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
为偶数,则必须为&gt; = 4。
如果N
为奇数,则必须为&gt; = 9.此外,R
和C
必须为&gt; = 3.
否则没有解决方案。
如果N
是偶数且您无法用非地雷填充至少两行,请使用N / 2
非地雷填充前两行。
如果N
是奇数,你不能用非地雷填充至少两行,而用3个非地雷填充第三行,那么用(N - 3) / 2
非地雷填充前两行第三排有3个非地雷。
如果N % C = 1
,请将最后一个非矿区从最后一个完整行移动到下一行。
R=5
,C=5
,M=9
:
c....
.....
....*
..***
*****
可以编写一个实现这些规则的算法,并在O(1)
中返回结果矿场的描述。当然,绘制网格需要O(R*C)
。我还根据Code Jam Judge接受的这些想法在Perl中编写了一个实现。
答案 1 :(得分:3)
这是我的code。我解决了不同的情况,例如number of rows=1
或number of columns=1
或number 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个空格。只需单击左上角即可完成。
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)
我解决这个问题的方法如下:
总之,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)
用所有地雷填充网格,然后点击任意位置。
按顺序向左/向右(或向上/向下)填充:点击,非地雷,地雷(例如c...****
)。
不可能
我开始使用“空”网格(所有.
s)并将点击放在一个角落(我将使用左上角点击,并开始从右下角填充地雷)。
我们将R1
和C1
用作“当前”行和列。
虽然我们有足够的地雷来填充行或列,当移除时,不会留下单个行或列(while((M >= R1 && C1 > 2) || (M >= C1 && R1 > 2))
),我们“修剪”网格(填充地雷并减少{{ 1}}或R1
)使用最短边并移除那么多地雷。因此,留下6个地雷的4x5将成为剩余2个地雷的4x4。
C1
,如果是这样的话,我们需要从最短的边缘将一个矿井放入一行或一列,然后用剩余的矿井填充最短的边缘。我将显示我使用数字进入网格的订单,以帮助进行可视化
M == min(R1,C1)-1
R = 7, C = 6, M = 29
我花了几个不同的尝试来使我的算法正确,但是我用PHP编写了我的算法并得到了小而大的正确。
答案 8 :(得分:0)
我也在这个问题上试过运气,但由于某些原因没有通过检查。
我认为如果少于(行* cols-4)地雷,它可以解决(3x3矩阵),因为我只需要4个单元用于“c”,其边界为“。”
我的算法如下:
<强>可解强>:
rows*cols - 4 == maximum mines
)构建解决方案
rows*cols matrix
,默认值为nil m[0][0]
并指定'c'
m[0][0]
'.'
个环境
'*'
,直到地雷结束,然后分配'.'
答案 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大小的板 合理的时间并且更容易实施(即,不需要 枚举角落/棘手的案例)。
如果比赛时间较短,我们可能没有足够的时间 枚举所有可能的情况。在这种情况下,投注 回溯算法(或任何其他更容易的算法) 实施)可能是一个好主意。找到这样的算法是一门艺术:)。