炸弹掉落算法


212

我有一个n x m由非负整数组成的矩阵。例如:

2 3 4 7 1
1 5 2 6 2
4 3 4 2 1
2 1 2 4 1
3 1 3 4 1
2 1 4 3 2
6 9 1 6 4

“投下炸弹”将目标单元及其所有八个邻居的数量减少一,最小为零。

x x x 
x X x
x x x

有什么算法可以确定将所有像元减少到零所需的最小炸弹数量?

B选项(由于我不是认真的读者)

实际上,问题的第一个版本不是我正在寻找答案的版本。我没有仔细阅读整个任务,还有其他限制,让我们说:

关于简单问题,当行中的序列必须不增加时:

8 7 6 6 5 可能的输入顺序

7 8 5 5 2 这是不可能的,因为7-> 8依次增长。

也许找到“更轻松”案例的答案将有助于找到更困难的解决方案。

PS:我相信,当我们在几种情况下都需要最少的炸弹才能清除上限时,我们选择在行的“左侧”使用最多炸弹的炸弹。还有任何证明可能是正确的吗?


4
好吧,我发现有些字段可以像示例2 3 1 5那样跳过,将其放在2,3,1上是没有意义的,因为删除它们会导致某些子集损坏,而我们可以通过删除5来造成这些损坏。但是不能查找如何使其在全球范围内有效(如果正确的话)。清除2要求使用2枚投掷在任何邻居上的炸弹,而5枚则包含其他几组伤害。但是后来我不知道以后要做什么,因为当您重写它(减少后)时,您有两种选择(没有一套超大的损害)。
2013年

23
这个NP难道是偶然的吗?它似乎是“ 最大覆盖问题”的一个变体。
Mysticial

14
+1给我一些有趣的思考
Nick Mitchinson

3
@Kostek,大问题!请发布链接。
Panic Panic

5
也许您应该澄清一下,您说的问题是:what's the minimum amount of bombs required to clean the board?这是否意味着不一定要找到实际的轰炸模式,而只需找到最少的炸弹?
Lie Ryan

Answers:


38

有一种方法可以将其简化为一个简单的子问题。

解释分为两部分:算法和算法提供最佳解决方案的原因。没有第二个就没有第一个意义,所以我将从为什么开始。

如果您想轰炸矩形(假设是一个大矩形-尚无边缘情况),您会看到将周长正方形的空心矩形减小为0的唯一方法是轰炸周长或轰炸矩形的空心矩形。正方形刚好在周长内。我将其称为外围层1,并将其内部的矩形称为层2。

一个重要的见解是没有第1层的点轰炸,因为这样做的“爆炸半径”始终包含在第2层另一个正方形的爆炸半径之内。您应该能够轻松地说服自己。

因此,我们可以减少问题,找到轰炸外围的最佳方法,然后我们可以重复进行直到所有平方均为0。

但是,当然,如果能够以一种不太理想的方式炸毁外围,那将不会总是找到最佳解决方案,但是通过使用X个额外的炸弹,可以使> X炸弹简化减少内层的问题。因此,如果我们将许可层称为第一层,如果将额外的X炸弹放置在第二层的某个位置(仅在第一层的内部),是否可以将以后轰炸第二层的工作量减少X倍呢?换句话说,我们必须证明我们在缩小外围方面会很贪心。

但是,我们确实知道我们可以贪婪。因为第2层中没有炸弹比第3层中策略性放置的炸弹更能有效地将第2层减少为0。并且由于与以前相同的原因-我们始终可以在第3层中放置炸弹,它将影响每个正方形放置在第2层中的炸弹可以攻击的第2层。因此,贪婪永远不会伤害我们(在这种贪婪的意义上)。

因此,我们要做的就是找到通过轰炸下一个内层将许可者减少到0的最佳方法。

首先将角向0轰炸不会对我们造成伤害,因为只有内层的角才能到达角,所以我们真的别无选择(而且,能够到达角的周界上的任何炸弹都具有爆炸半径内层角的爆炸半径)。

完成此操作后,与内角0相邻的正方形只能与内层相距2个正方形:

0       A       B

C       X       Y

D       Z

在这一点上,周长实际上是一个封闭的一维环路,因为任何炸弹都会减少3个相邻的正方形。除了拐角处有些怪异-X可以“命中” A,B,C和D。

现在我们不能使用任何爆炸半径技巧-每个正方形的情况都是对称的,除了怪异的角,而且甚至没有爆炸半径是另一个的子集。请注意,如果这是一条直线(如Panic Panic上校所讨论),而不是一个闭环,则解决方案是微不足道的。端点必须减小为0,并且炸弹半径是超集,因此永远不会伤害炸毁端点附近的点。将端点设置为0后,您仍将拥有一个新的端点,因此重复(直到该行全为0)。

因此,如果我们可以将图层中的单个正方形最佳地减小为0,则可以使用一种算法(因为我们已经剪切了循环,现在有了一条带有端点的直线)。我认为,以最低值(给您2个选项)在正方形附近进行轰炸是最佳的(使您可能必须分割轰炸才能进行管理),使该最小值附近的2个正方形内的最大值成为可能。 (还没有?)没有证据。


+1-我打算写类似的东西。我认为您已经掌握了!
Rex Kerr

5
@beaker,请仔细阅读问题。炸毁一个正方形会减少其所有八个邻居,因此他的假设实际上是正确的。
darksky 2013年

20
But, we do know we can be greedy...-我不买这个。考虑1 1 2 1 1 2周长。炸弹的最小数量为4,但是有三种不同的解决方案。 每个解决方案对下一层都有不同的影响。只要一个边界有多个最小解决方案,就不能不考虑内部层完全隔离该边界。我真的认为不回溯就无法解决这个问题。
2013年

4
我当时在考虑这种解决方案,但是看起来很简单。的确,您可以在第2层投下炸弹以清洁第1层,但是如果有多个解决方案,它们会影响更高层的解决方案。
卢卡·拉涅

12
@psr:这不起作用。对于外层最佳的轰炸方法可能不是全局最佳的。范例:0011100 0100010 0000000 0000000 1110111。轰炸第一层的最佳方法是在第二排中间轰炸,总共要用三枚炸弹杀死外层。但是随后您需要两枚炸弹来照顾下一层。最佳只需要总共炸弹四枚:前两排两枚,后排两枚。
nneonneo

26

波利亚说:“如果您不能解决问题,那么您可以解决一个更简单的问题:找到它。”

明显的简单问题是一维问题(当网格为单行时)。让我们从最简单的算法开始-贪婪地轰炸最大的目标。什么时候出错?

给出1 1 1,贪婪算法对于首先炸弹哪个单元格无关紧要。当然,中心单元更好-一次将所有三个单元归零。这暗示了新的算法A,“炸弹以使剩余的总和最小化”。该算法何时会出错?

给定1 1 2 1 1,算法A在轰炸第二,第三或第四单元之间是无关紧要的。但是轰炸第二个牢房0 0 1 1 1要比轰炸第三个牢房要好1 0 1 0 1。如何解决?轰炸第三个牢房的问题是,它使我们只能向左工作,而向右工作,这必须分开进行。

怎么样“炸弹使剩余的总和最小化,但使左侧(我们轰炸的地方)的最小值与右侧的最小值最大化”。将此算法称为B。此算法何时会出错?


编辑:阅读评论后,我同意一个更有趣的问题是将一维问题更改为两端连接在一起。希望看到任何进展。


40
我不确定为什么这个答案会引起如此多的反对-一维案例几乎是微不足道的,只是总是在第一个正数元素的右边轰炸该元素。之所以起作用,是因为总是有一种最佳方法来轰炸左侧仅包含0的任何元素。可以将其扩展到2D以最佳地去除角部正方形,但是我没有找到一种明显的方法来扩展该范围。
BlueRaja-Danny Pflughoeft13年

3
@BlueRaja,我表示赞成,因为它清楚地说明了其他答案中讨论的贪婪方法是不够的(至少,它需要补充其他标准)。目标的某些选择,即使它们导致总数的平均减少,也可能使事情分散。我认为这对于2D问题很有用。
蒂姆·古德曼

3
通常,“如果您卡在2D外壳上,请先尝试1D外壳”是一个很好的建议。
蒂姆·古德曼

21
@Tim:“'先尝试一维案例'是个好建议”是的,这将是一个很好的评论;但这不是答案 ……
BlueRaja-Danny Pflughoeft13年

3
我确实认为您有一个很好的观点,尽管一维案例在这里可能有点误导,因为它有一个简单的解决方案,无法轻易扩展到更高的尺寸。我认为一维具有周期性边界条件的情况(环绕情况)可能会更好。
蒂姆·古德曼

12

自从我没时间以来,我只需要停止部分解决方案,但是希望即使是部分解决方案也可以为解决该问题的一种潜在方法提供一些见解。

当遇到困难的问题时,我喜欢提出一些更简单的问题,以便对问题空间有一个直觉。在这里,我采取的第一步是将2D问题简化为1D问题。考虑一行:

0 4 2 1 3 0 1

不知何故,您知道您需要在该4地点或其周围轰炸4次才能将其降至0。由于该地点左侧的炸弹较小,因此轰炸04过度轰炸没有任何好处2。事实上,我相信(但缺乏严格的证明),其轰炸2,直到4点下降到0至少不比任何其他策略来获取4到0人们可以继续向下的战略左到右行像这样:

index = 1
while index < line_length
  while number_at_index(index - 1) > 0
    bomb(index)
  end
  index++
end
# take care of the end of the line
while number_at_index(index - 1) > 0
  bomb(index - 1)
end

几个轰炸命令样本:

0 4[2]1 3 0 1
0 3[1]0 3 0 1
0 2[0]0 3 0 1
0 1[0]0 3 0 1
0 0 0 0 3[0]1
0 0 0 0 2[0]0
0 0 0 0 1[0]0
0 0 0 0 0 0 0

4[2]1 3 2 1 5
3[1]0 3 2 1 5
2[0]0 3 2 1 5
1[0]0 3 2 1 5
0 0 0 3[2]1 5
0 0 0 2[1]0 5
0 0 0 1[0]0 5
0 0 0 0 0 0[5]
0 0 0 0 0 0[4]
0 0 0 0 0 0[3]
0 0 0 0 0 0[2]
0 0 0 0 0 0[1]
0 0 0 0 0 0 0

从一个需要以某种方式下降的数字开始的想法很吸引人,因为突然有可能找到一种解决方案,有人认为它至少与所有其他解决方案一样好

在复杂的下一步向上其中该搜索的至少一样好的仍然可行是在板的边缘。对我来说很明显,轰炸外缘从来没有任何严格的好处。您最好将其中一个炸掉,免费获得另外三个空间。鉴于此,我们可以说轰炸边缘内的一个环至少与轰炸边缘一样好。此外,我们可以将其与直觉相结合,即轰击边缘的右侧实际上是将边缘空间减小到0的唯一方法。更重要的是,找出最佳策略非常简单(因为在最小角点数)将角点数降低到0。我们将所有这些放在一起,可以更接近二维空间中的解决方案。

鉴于对角点的观察,我们可以肯定地说,我们知道从任何起始板到所有角均为零的板的最佳策略。这是这种木板的一个例子(我从上面的两个线性木板上借了数字)。我给一些空格贴上了不同的标签,我将解释原因。

0 4 2 1 3 0 1 0
4 x x x x x x 4
2 y y y y y y 2
1 y y y y y y 1
3 y y y y y y 3
2 y y y y y y 2
1 y y y y y y 1
5 y y y y y y 5
0 4 2 1 3 0 1 0

人们会注意到,最上面一行确实与我们之前看到的线性示例非常相似。回顾我们先前的观察,使第一行全部降低到0的最佳方法是轰炸第二行(该x行)。没有办法通过轰炸任何一排来清理最上面的一排,y并且没有轰炸相较于该x行上相应空间轰炸最上面一排的额外好处。

我们可以从上方应用线性策略(轰炸该x行上的相应空间),将自己放在第一行,而没有其他问题。它会像这样:

0 4 2 1 3 0 1 0
4 x[x]x x x x 4
2 y y y y y y 2
1 y y y y y y 1
3 y y y y y y 3
2 y y y y y y 2
1 y y y y y y 1
5 y y y y y y 5
0 4 2 1 3 0 1 0

0 3 1 0 3 0 1 0
4 x[x]x x x x 4
2 y y y y y y 2
1 y y y y y y 1
3 y y y y y y 3
2 y y y y y y 2
1 y y y y y y 1
5 y y y y y y 5
0 4 2 1 3 0 1 0

0 2 0 0 3 0 1 0
4 x[x]x x x x 4
2 y y y y y y 2
1 y y y y y y 1
3 y y y y y y 3
2 y y y y y y 2
1 y y y y y y 1
5 y y y y y y 5
0 4 2 1 3 0 1 0

0 1 0 0 3 0 1 0
4 x[x]x x x x 4
2 y y y y y y 2
1 y y y y y y 1
3 y y y y y y 3
2 y y y y y y 2
1 y y y y y y 1
5 y y y y y y 5
0 4 2 1 3 0 1 0

0 0 0 0 3 0 1 0
4 x x x x x x 4
2 y y y y y y 2
1 y y y y y y 1
3 y y y y y y 3
2 y y y y y y 2
1 y y y y y y 1
5 y y y y y y 5
0 4 2 1 3 0 1 0

在最后两次爆炸中,这种方法的缺陷非常明显。很明显,因为这降低了唯一的爆炸点4在第二行的第一列数字是第一xy。最后两次轰炸显然不如只轰炸第一次轰炸x,后者会做的完全一样(关于第一排的第一个炸弹,我们没有其他清除方法)。由于我们已经证明我们当前的策略是次优的,因此显然需要对策略进行修改。

在这一点上,我可以降低复杂性,只关注一个角落。让我们考虑一下这个:

0 4 2 1
4 x y a
2 z . .
1 b . .

很明显地想把空间的唯一途径4降至零是轰炸的某种组合xyz。我心中有一些杂技,我很确定最佳的解决方案是先轰炸x3次,然后a再轰炸b。现在,需要弄清楚我是如何达到该解决方案的,如果它能揭示出任何直觉,我们甚至可以用来解决这个本地问题。我注意到这里没有炸弹y和炸弹z。试图找到一个炸弹轰炸这些空间有意义的角落,将产生一个看起来像这样的角落:

0 4 2 5 0
4 x y a .
2 z . . .
5 b . . .
0 . . . .

对于我来说,很明显,最佳解决方案是轰炸y5次和z5次。让我们更进一步。

0 4 2 5 6 0 0
4 x y a . . .
2 z . . . . .
5 b . . . . .
6 . . . . . .
0 . . . . . .
0 . . . . . .

这里,感觉类似地直观的,最优的解决方案是轰炸ab6次,然后x4倍。

现在,它变成了如何将这些直觉转化为我们可以建立的原则的游戏。

希望继续!


10

对于更新的问题,简单的贪心算法给出了最佳结果。

将A [0,0]炸弹放到单元格A [1,1],然后将A [1,0]炸弹放到单元格A [2,1],然后继续向下进行此过程。要清洁左下角,请将炸弹max(A [N-1,0],A [N-2,0],A [N-3,0])炸弹放到单元格A [N-2,1]中。这将完全清除前3列。

使用相同的方法,先清理第3、4、5列,然后清理第6、7、8列等。

不幸的是,这无助于找到原始问题的解决方案。


可以证明“更大”的问题(没有“不断增加”的约束)是NP难的。这是证明的草图。

假设我们有一个度数为3的平面图。让我们找到该图的最小顶点覆盖。根据Wikipedia的文章,对于度数最大为3的平面图,此问题是NP难题。这可以通过从Planar 3SAT进行归约来证明。平面3SAT的硬度-从3SAT降低。这两个证明在教授最近在“算法下界”中的演讲中都有介绍。Erik Demaine(第7和第9讲)。

如果我们拆分原始图的某些边(图上的左图),每个边上有偶数个附加节点,则生成的图(图上的右图)应具有与原始顶点完全相同的最小顶点覆盖。这种变换允许将图形顶点对齐到网格上的任意位置。

enter image description here

如果我们仅将图形顶点放置在偶数行和列上(以使没有两个入射到一个顶点的边形成锐角的方式),请在有边的任何地方插入“ 1”,并在其他网格位置插入“ 0”,我们可以对原始问题使用任何解决方案来找到最小的顶点覆盖率。


左图从何而来?抱歉,我不太理解您的解释!
ryyst 2013年

1
@ryyst:左图只是平面图的一个示例。它用于演示如何将最大4度的平面图转换为网格对齐图,然后转换为n * m矩阵。应用于该矩阵的“炸弹掉落”算法将解决此变换后的图以及因此该“左”图的顶点覆盖问题。
Evgeny Kluev 2013年

嗯,我现在明白了,我相信您的转换是正确的。谢谢!
ryyst 2013年

@EvgenyKluev,我想现在您需要证明对于“度为4的平面图”,顶点覆盖仍然是NP-hard的。
Shahbaz 2013年

@Shahbaz:恐怕这个证明太长了。因此,我在证明中添加了链接。
Evgeny Kluev

9

您可以将此问题表示为整数编程问题。(这只是解决此问题的可能解决方案之一)

有几点:

a b c d
e f g h
i j k l
m n o p

一个人可以写出16个方程,其中对于点f成立

f <= ai + bi + ci + ei + fi + gi + ii + ji + ki   

最小化所有索引和整数解的总和。

解决方案当然是这些指标的总和。

通过将所有xi设置在边界0上,可以进一步简化此操作,因此在此示例中最终得到4 + 1方程。

问题在于,没有解决这些问题的简单算法。我不是这方面的专家,但是由于线性编程很难解决这个问题。


8
NP中的所有问题都可以表述为整数编程问题,因此这并不是很有帮助,除非我们已经知道问题是NP完成
BlueRaja-Danny Pflughoeft

1
我同意。也不必知道为了知道什么解决方案而必须进行的精确动作。
卢卡·拉涅

1
当你的边界设置为0,不平等的数量仍然是16
darksky

9

这是部分答案,我试图找到可能存在炸弹数量的下限和上限。

在3x3及更小的电路板上,该解决方案通常都是编号最大的单元。

在大于4x4的电路板上,第一个明显的下界是边角的总和:

*2* 3  7 *1*
 1  5  6  2
 2  1  3  2
*6* 9  6 *4*

但是,如果您安排炸弹,则不可能用少于2 + 1 + 6 + 4 = 13枚炸弹来清除此4x4棋盘。

在其他答案中已经提到,将炸弹放在拐角处以消除拐角永远不会比将炸弹放在拐角本身更糟,因此请考虑以下情况:

*2* 3  4  7 *1*
 1  5  2  6  2
 4  3  4  2  1
 2  1  2  4  1
 3  1  3  4  1
 2  1  4  3  2
*6* 9  1  6 *4*

我们可以通过在第二个拐角处放置炸弹以提供新的板块来消除角落:

 0  1  1  6  0
 0  3  0  5  1
 2  1  1  1  0
 2  1  2  4  1
 0  0  0  0  0
 0  0  0  0  0
 0  3  0  2  0

到目前为止,一切都很好。我们需要13枚炸弹才能清除角落。

现在观察下面标记的数字6、4、3和2:

 0  1  1 *6* 0
 0  3  0  5  1
 2  1  1  1  0
*2* 1  2 *4* 1
 0  0  0  0  0
 0  0  0  0  0
 0 *3* 0  2  0

没有办法炸弹 使用单个炸弹任何两个炸弹,因此最小炸弹增加了6 + 4 + 3 + 2,因此,加上我们过去用于清理弯道的炸弹数量,我们得到了最小炸弹该地图所需的炸弹数量已变为28枚。少于28枚炸弹无法清除此地图,这是该地图的下限。

您可以使用贪心算法来建立上限。其他答案表明,贪婪算法会产生使用28枚炸弹的解决方案。由于我们早先已经证明了最佳解决方案不能少于28枚炸弹,因此28枚炸弹确实是最佳解决方案。

当贪婪和我上面提到的找到最小界限的方法没有收敛时,我想您必须回到检查所有组合的方式。

查找下限的算法如下:

  1. 选择编号最高的元素,将其命名为P。
  2. 将所有与P和P本身相距两步的单元标记为不可拾取。
  3. 将P添加到minimums列表中。
  4. 重复步骤1,直到所有单元格都无法拾取。
  5. minimums列表求和以得到下限。

9

这将是一种贪婪的方法:

  1. 计算n x m阶的“得分”矩阵,其中score [i] [j]是如果位置(i,j)被炸毁则矩阵中点的总扣除量。(一个点的最高分是9,最低分是0)

  2. 明智地按行移动,找到并选择得分最高的第一个位置(例如(i,j))。

  3. 炸弹(i,j)。增加炸弹数量。

  4. 如果原始矩阵的所有元素都不为零,则转到1。

我怀疑这是否是最佳解决方案。

编辑:

我上面发布的贪婪方法虽然有效,但很可能无法为我们提供最佳解决方案。所以我想应该在其中添加一些DP元素。

我认为我们可以同意,在任何时间点,都必须将“得分”最高的位置之一(score [i] [j] =如果(i,j)被炸毁,则减去总得分)作为目标。从这个假设开始,这是新方法:

NumOfBombs(M):(返回所需的最小轰炸次数)

  1. 给定一个n X m阶的矩阵M。如果M的所有元素均为零,则返回0。

  2. 计算“得分”矩阵M。

    令k个不同的位置P1,P2,... Pk(1 <= k <= n * m)为M中得分最高的位置。

  3. return(1 +分钟(NumOfBombs(M1),NumOfBombs(M2),...,NumOfBombs(Mk)))

    如果我们分别轰炸位置P1,P2,...,Pk,则M1,M2,...,Mk是所得矩阵。

另外,如果我们希望除此以外的位置顺序也要核对,则必须跟踪“ min”的结果。


3
我不知道将分数设置为当前值的总和是否会产生更好的结果。这实际上将更有效地打平地面。
尤金(Eugene)2013年

@Eugene:非常有趣的一点。我想不出您的方式不能产生更好结果的原因...
SidR 2013年

@Eugene:也许附近的当前值之和可以用于“优先”度量?核弹与最高得分和最高优先级的节点..
锡德

只需阅读此答案,我认为它类似于我刚刚发布的第二个答案(也许在我的答案中说明了更多)。我认为,如果总有一个得分最大的空格,那是最佳选择,因为可以确保每次轰炸都可能产生最大的影响。爆炸的顺序无关紧要,因此在每个步骤中选择最佳炸弹应该是最佳的。但是由于可能存在“最佳”联系,因此可能需要最佳解决方案,因此您需要回溯并在出现领带时同时尝试两者。
Tim Goodman

1
@Eugene,也许我没有关注您。最大减少量和所有剩余值的最小和之间有什么区别?剩余价值的总和(轰炸后)仅是当前总价值减去轰炸该空间所产生的减少,那么这些不是吗?
Tim Goodman

8

您的问题具有行之间不变的值,因此很容易解决。

请注意,左列包含最高的数字。因此,任何最佳解决方案都必须首先将该列减小为零。因此,我们可以对该列执行一轰炸,将其中的每个元素减少为零。我们让炸弹落在第二列上,以便最大程度地造成伤害。我认为,这里有很多关于一维案例的文章,所以跳过该案例我感到很安全。(如果您想让我描述一下,我可以。)由于减少的属性,最左边的三列将全部减少为零。但是,在这里我们可以证明使用最少数量的炸弹,因为左列必须清零。

现在,一旦左列被归零,我们就修剪掉现在归零的最左边的三列,并使用现在归约的矩阵重复。这必须为我们提供最佳解决方案,因为在每个阶段,我们都使用最少数量的炸弹。


我知道了。我想到了类似的想法。:S下次我将更仔细地阅读。但是,由于这个原因,许多人都需要解决“好”的问题。
2013年

4

使用分支定界的Mathematica整数线性规划

正如已经提到的,可以使用整数线性规划(即NP-Hard)解决此问题。Mathematica已经内置了ILP。"To solve an integer linear programming problem Mathematica first solves the equational constraints, reducing the problem to one containing inequality constraints only. Then it uses lattice reduction techniques to put the inequality system in a simpler form. Finally, it solves the simplified optimization problem using a branch-and-bound method."[请参见Mathematica中的约束优化教程。

我编写了以下代码,它们利用了Mathematica的ILP库。它出奇地快。

solveMatrixBombProblem[problem_, r_, c_] := 
 Module[{}, 
  bombEffect[x_, y_, m_, n_] := 
   Table[If[(i == x || i == x - 1 || i == x + 1) && (j == y || 
        j == y - 1 || j == y + 1), 1, 0], {i, 1, m}, {j, 1, n}];
  bombMatrix[m_, n_] := 
   Transpose[
    Table[Table[
      Part[bombEffect[(i - Mod[i, n])/n + 1, Mod[i, n] + 1, m, 
        n], (j - Mod[j, n])/n + 1, Mod[j, n] + 1], {j, 0, 
       m*n - 1}], {i, 0, m*n - 1}]];
  X := x /@ Range[c*r];
  sol = Minimize[{Total[X], 
     And @@ Thread[bombMatrix[r, c].X >= problem] && 
      And @@ Thread[X >= 0] && Total[X] <= 10^100 && 
      Element[X, Integers]}, X];
  Print["Minimum required bombs = ", sol[[1]]];
  Print["A possible solution = ", 
   MatrixForm[
    Table[x[c*i + j + 1] /. sol[[2]], {i, 0, r - 1}, {j, 0, 
      c - 1}]]];]

对于问题中提供的示例:

solveMatrixBombProblem[{2, 3, 4, 7, 1, 1, 5, 2, 6, 2, 4, 3, 4, 2, 1, 2, 1, 2, 4, 1, 3, 1, 3, 4, 1, 2, 1, 4, 3, 2, 6, 9, 1, 6, 4}, 7, 5]

产出

在此处输入图片说明

对于任何使用贪婪算法阅读本文的人

请尝试解决以下10x10问题:

5   20  7   1   9   8   19  16  11  3  
17  8   15  17  12  4   5   16  8   18  
4   19  12  11  9   7   4   15  14  6  
17  20  4   9   19  8   17  2   10  8  
3   9   10  13  8   9   12  12  6   18  
16  16  2   10  7   12  17  11  4   15  
11  1   15  1   5   11  3   12  8   3  
7   11  16  19  17  11  20  2   5   19  
5   18  2   17  7   14  19  11  1   6  
13  20  8   4   15  10  19  5   11  12

在这里用逗号分隔:

5, 20, 7, 1, 9, 8, 19, 16, 11, 3, 17, 8, 15, 17, 12, 4, 5, 16, 8, 18, 4, 19, 12, 11, 9, 7, 4, 15, 14, 6, 17, 20, 4, 9, 19, 8, 17, 2, 10, 8, 3, 9, 10, 13, 8, 9, 12, 12, 6, 18, 16, 16, 2, 10, 7, 12, 17, 11, 4, 15, 11, 1, 15, 1, 5, 11, 3, 12, 8, 3, 7, 11, 16, 19, 17, 11, 20, 2, 5, 19, 5, 18, 2, 17, 7, 14, 19, 11, 1, 6, 13, 20, 8, 4, 15, 10, 19, 5, 11, 12

对于这个问题,我的解决方案包含208枚炸弹。这是一个可能的解决方案(我能够在大约12秒内解决此问题)。

在此处输入图片说明

为了测试Mathematica产生的结果,请查看您的贪婪算法是否可以做得更好。



3

无需将问题转换为线性子问题。

取而代之的是使用简单的贪婪启发式方法,即从最大的角开始,轰炸角落

在给定的示例中,有四个角{2、1、6、4}。对于每个角落,没有比对角对角格炸弹更好的举动,因此我们知道,我们的第一个2 + 1 + 6 + 4 = 13次轰炸必须在这些对角格内。进行轰炸后,我们得到一个新的矩阵:

2 3 4 7 1      0 1 1 6 0      0 1 1 6 0     1 1 6 0     0 0 5     0 0 0 
1 5 2 6 2      0 3 0 5 1      0 3 0 5 1  => 1 0 4 0  => 0 0 3  => 0 0 0  
4 3 4 2 1      2 1 1 1 0      2 1 1 1 0     0 0 0 0     0 0 0     0 0 3  
2 1 2 4 1  =>  2 1 2 4 1  =>  2 1 2 4 1     0 0 3 0     0 0 3      
3 1 3 4 1      0 0 0 0 0      0 0 0 0 0 
2 1 4 3 2      0 0 0 0 0      0 0 0 0 0 
6 9 1 6 4      0 3 0 2 0      0 0 0 0 0 

在前13次轰炸之后,我们使用启发式方法通过三次轰炸消除3 0 2。现在,我们在第4行有2个新角,{2,1}。我们轰炸了这3次炸弹 现在,我们将矩阵缩小为4 x 4。在左上角有一个角。我们炸了。现在我们剩下两个角,{5,3}。由于5是最大的角落,因此我们首先轰炸5次炸弹,然后再轰炸另一个角落的3个炸弹。总数为13 + 3 + 3 + 1 + 5 + 3 = 28。


1
我不明白在炸毁角落后您通常会怎么做
RiaD 2013年

轰炸角落永远比从角落对角向内轰炸更有效。
psr

1
psr,您误解了我的帖子,我在对角线对角轰炸,重新阅读了该帖子
Tyler Durden 2013年

11
@TylerDurden:这仅适用于矩阵较小的情况。在较大的矩阵上,轰炸角落后,通常将无法再切割边缘。
Lie Ryan

3

这将通过这种“迷宫”位置进行最短路径(一系列轰炸)的广度搜索。不,我无法证明没有更快的算法,对不起。

#!/usr/bin/env python

M = ((1,2,3,4),
     (2,3,4,5),
     (5,2,7,4),
     (2,3,5,8))

def eachPossibleMove(m):
  for y in range(1, len(m)-1):
    for x in range(1, len(m[0])-1):
      if (0 == m[y-1][x-1] == m[y-1][x] == m[y-1][x+1] ==
               m[y][x-1]   == m[y][x]   == m[y][x+1] ==
               m[y+1][x-1] == m[y+1][x] == m[y+1][x+1]):
        continue
      yield x, y

def bomb(m, (mx, my)):
  return tuple(tuple(max(0, m[y][x]-1)
      if mx-1 <= x <= mx+1 and my-1 <= y <= my+1
      else m[y][x]
      for x in range(len(m[y])))
    for y in range(len(m)))

def findFirstSolution(m, path=[]):
#  print path
#  print m
  if sum(map(sum, m)) == 0:  # empty?
    return path
  for move in eachPossibleMove(m):
    return findFirstSolution(bomb(m, move), path + [ move ])

def findShortestSolution(m):
  black = {}
  nextWhite = { m: [] }
  while nextWhite:
    white = nextWhite
    nextWhite = {}
    for position, path in white.iteritems():
      for move in eachPossibleMove(position):
        nextPosition = bomb(position, move)
        nextPath = path + [ move ]
        if sum(map(sum, nextPosition)) == 0:  # empty?
          return nextPath
        if nextPosition in black or nextPosition in white:
          continue  # ignore, found that one before
        nextWhite[nextPosition] = nextPath

def main(argv):
  if argv[1] == 'first':
    print findFirstSolution(M)
  elif argv[1] == 'shortest':
    print findShortestSolution(M)
  else:
    raise NotImplementedError(argv[1])

if __name__ == '__main__':
  import sys
  sys.exit(main(sys.argv))

1
该算法找到最少的移动次数,但是可能需要很长时间。您是否已在给定的数据集上运行此程序?这将为其他算法提供基准。
瑞安·阿莫斯

1
给定矩阵的5x4子集在大约2秒内解决,而5x5已经用了2分钟。我还没有尝试更多;-)是的,除了原始任务之外,该算法并未针对其他任何东西进行优化:找到最短的解决方案。
Alfe 2013年

2
这就是指数复杂性的美。
瑞安·阿莫斯

3

线性编程方法在这里似乎很有帮助。

P m xn为具有位置值的矩阵:

职位矩阵

现在让我们定义一个炸弹矩阵B(x,y)mxn,其中1≤X≤米1个≤ÿ≤Ñ如下

炸弹矩阵

以这样的方式

炸弹矩阵中的位置值

例如:

B(3,3)

因此我们正在寻找矩阵B m xn = [ b ij ]

  1. 可以定义为炸弹矩阵的总和:

    B作为炸弹矩阵的总和

    q ij将是我们将在位置p ij放下的炸弹数量

  2. p IJ - B IJ ≤0 (更succint,让我们说,这是P - B≤0

同样,B应该使和最小炸弹总数

我们还可以将B作为前面的丑陋矩阵写成:

B作为数量总和的矩阵

并且由于P-B≤0(这意味着P≤B),因此下面具有以下线性线性不等式系统:

炸弹投掷数量与位置值之间的关系

作为q MN X 1定义为

数量向量

p mn x 1定义为

P的值作为向量分布

我们可以说我们有一个系统以下系统表示为矩阵的乘积http://latex.codecogs.com/gif.download?S%5Cmathbf%7Bq%7D&space;%5Cge&space;%5Cmathbf%7Bp%7DS mn x mn求逆矩阵求解系统。我自己没有扩展它,但是我相信用代码来完成它应该很容易。

现在,我们有一个最小的问题,可以说是

我们必须解决的系统

我相信使用简单算法之类的东西来解决是一件容易的事,几乎是微不足道的(关于它的文档很酷)。但是,我确实不知道任何线性编程(我将在Coursera上学习它,但是这只是在将来...),我在尝试理解它时有些头疼,并且我有大量的自由职业要完成,所以我只是在这里放弃。它可以是我做错了什么在某些时候,或者它不能再往前走,但我相信这条道路最终导致解决方案。无论如何,我很期待您的反馈。

(特别感谢这个令人惊叹的网站从LaTeX表达式创建图片


您确定不平等不会逆转吗?那Sq> = P?也就是说,一个正方形被轰炸的总次数大于或等于给定的矩阵。
darksky 2013年

1
当线性程序的变量限制为整数时,我们称其为“整数线性程序设计”(IP)。与连续情况不同,IP是NP-Complete。不幸的是,除非接受近似值,否则单纯形算法无济于事。IP 已经在另一个答案中提到
BlueRaja-Danny Pflughoeft 2013年

@ BlueRaja-DannyPflughoeft正确。"Despite the many crucial applications of this problem, and intense interest by researchers, no efficient algorithm is known for it.请参见第254页。整数线性规划是一个非常困难的计算问题。我们唯一希望提高效率的方法是利用矩阵S的内在属性。这毕竟不是那么任意。
Darksky 2013年

3

这个贪婪的解决方案似乎是正确的

正如评论中指出的那样,它将在2D中失败。但是也许您可以改善它。

对于1D:
如果至少有2个数字,则无需拍摄到最左边的一个,因为拍摄到第二个并不差。因此,拍摄到第二个,而第一个不为0,因为您必须这样做。移至下一个单元格。不要忘记最后一个单元格。

C ++代码:

void bombs(vector<int>& v, int i, int n){
    ans += n;
    v[i] -= n;
    if(i > 0)
        v[i - 1] -= n;
    if(i + 1< v.size())
        v[i + 1] -= n;
}

void solve(vector<int> v){
    int n = v.size();
    for(int i = 0; i < n;++i){
        if(i != n - 1){
            bombs(v, i + 1, v[i]);
        }
        else
            bombs(v, i, v[i])
    }
}

因此对于2D:
再次:您无需在第一行拍摄(如果有第二行)。因此,拍摄第二个。解决第一行的1D任务。(因为您需要将其设置为null)。下去。不要忘记最后一行。


5
反例:"0110","1110","1110"。你只需要1次射门,但我相信你的算法将用2
maniek

2

为了最小化炸弹数量,我们必须最大化每个炸弹的效果。为此,我们必须在每一步上选择最佳目标。对于每个点求和与它的八个邻居的总和-可以用作轰炸该点的有效数量。这将提供接近最佳的炸弹顺序。

UPD:我们还应该考虑零的数量,因为将它们无效地轰炸。实际上,问题在于使命中零的数目最小化。但是我们不知道有什么步骤可以使我们更接近这个目标。我同意这个问题是NP完全的想法。我建议采用一种贪婪的方法,这将给出接近真实的答案。


这不是最佳的。反例:10101010010100(上排,下排)你的方法将需要3。它可以在第2做
Mysticial

2

我相信要使炸弹数量最小化,您只需要使伤害最大化即可。为此,需要检查力最强的区域..因此,您首先要使用3x3内核分析磁场并检查总和更强..并在那里炸弹..直到场平..为此提交答案是28

var oMatrix = [
[2,3,4,7,1],
[1,5,2,6,2],
[4,3,4,2,1],
[2,1,2,4,1],
[3,1,3,4,1],
[2,1,4,3,2],
[6,9,1,6,4]
]

var nBombs = 0;
do
{
    var bSpacesLeftToBomb = false;
    var nHigh = 0;
    var nCellX = 0;
    var nCellY = 0;
    for(var y = 1 ; y<oMatrix.length-1;y++) 
        for(var x = 1 ; x<oMatrix[y].length-1;x++)  
        {
            var nValue = 0;
            for(var yy = y-1;yy<=y+1;yy++)
                for(var xx = x-1;xx<=x+1;xx++)
                    nValue += oMatrix[yy][xx];

            if(nValue>nHigh)
            {
                nHigh = nValue;
                nCellX = x;
                nCellY = y; 
            }

        }
    if(nHigh>0)
    {
        nBombs++;

        for(var yy = nCellY-1;yy<=nCellY+1;yy++)
        {
            for(var xx = nCellX-1;xx<=nCellX+1;xx++)
            {
                if(oMatrix[yy][xx]<=0)
                    continue;
                oMatrix[yy][xx] = --oMatrix[yy][xx];
            }
        }
        bSpacesLeftToBomb = true;
    }
}
while(bSpacesLeftToBomb);

alert(nBombs+'bombs');

这与其他一些答案的算法相同,但是要晚得多。
2013年

@psr不仅如此。这不是最佳的。
Mysticial

我发布了它,因为尽管提出了该算法,但没有发现任何代码或“概念教授”的帖子。所以我认为这可以帮助调解..但是.. btw @Mysticial您是否有建议说有更好的方法?
CaldasGSM

@CaldasGSM不用担心,原始问题(不进行排序)很难。到目前为止,只有一个答案可以最佳地解决它,但是它的运行时间是指数时间。
Mysticial

2

这是一个概括角点良好属性的解决方案。

假设我们可以为给定的字段找到一个完美的下降点,即降低其中值的最佳方法。然后,找到要投放的炸弹的最小数量,可以是算法的初稿(该代码是从ruby实现中复制粘贴的):

dropped_bomb_count = 0
while there_are_cells_with_non_zero_count_left
  coordinates = choose_a_perfect_drop_point
  drop_bomb(coordinates)
  dropped_bomb_count += 1
end
return dropped_bomb_count

挑战是 choose_a_perfect_drop_point。首先,让我们定义一个完美的下降点。

  • 一个着落点(x, y)降低了价值(x, y)。它还可能会降低其他单元格中的值。
  • 将一滴点一个(x, y)更好比滴点b(x, y),如果它在细胞中的适当超集减小的值即 b而减小。
  • 下降点是 如果没有其他更好的下降点,则最大
  • 两个下降点 (x, y)等效的减少相同的单元格集合,则它们的。
  • 如果的下降点等于的所有最大下降点,则它(x, y)完美(x, y)

如果有一个完美的下降点(x, y),则不能在以下位置减小该值(x, y)比将一个炸弹投到一个完美的下降点上更有效地(x, y)

给定字段的理想落点就是其任何单元格的理想落点。

以下是一些示例:

1 0 1 0 0
0 0 0 0 0
1 0 0 0 0
0 0 0 0 0
0 0 0 0 0

单元格(0, 0)(从零开始的索引)的理想下降点是(1, 1)。对于所有其他丢分(1, 1),那就是(0, 0)(0, 1)(1, 0),减少少的细胞。

0 0 0 0 0
0 0 0 0 0
0 0 1 0 0
0 0 0 0 0
0 0 0 0 0

用于小区A完美滴点(2, 2)(基于零的下标)是否(2, 2),以及所有的周围小区(1, 1)(1, 2)(1, 3)(2, 1)(2, 3)(3, 1)(3, 2),和(3, 3)

0 0 0 0 1
0 0 0 0 0
0 0 1 0 0
0 0 0 0 0
0 0 0 0 0

单元格的理想下降点(2, 2)(3, 1):减小in的值(2, 2),并减小in 的值(4, 0)。的所有其他下降点(2, 2)都不是最大的,因为它们减少了一个单元格。的完美落点(2, 2)也是的完美落点(4, 0),并且是该领域唯一的完美落点。这为该领域提供了完美的解决方案(投下一枚炸弹)。

1 0 0 0 0
0 0 0 0 0
0 0 1 0 0
0 0 0 0 0
1 0 0 0 0

没有完美的放置点(2, 2)(1, 1)和和(1, 3)减少(2, 2)以及另一个单元格(它们是的最大放置点(2, 2)),但是它们并不相等。但是,(1, 1)是的完美落点(0, 0)(1, 3)也是的完美落点(0, 4)

有了完美落点的定义和一定的检查顺序,对于问题中的示例,我得到以下结果:

Drop bomb on 1, 1
Drop bomb on 1, 1
Drop bomb on 1, 5
Drop bomb on 1, 5
Drop bomb on 1, 5
Drop bomb on 1, 6
Drop bomb on 1, 2
Drop bomb on 1, 2
Drop bomb on 0, 6
Drop bomb on 0, 6
Drop bomb on 2, 1
Drop bomb on 2, 5
Drop bomb on 2, 5
Drop bomb on 2, 5
Drop bomb on 3, 1
Drop bomb on 3, 0
Drop bomb on 3, 0
Drop bomb on 3, 0
Drop bomb on 3, 0
Drop bomb on 3, 0
Drop bomb on 3, 4
Drop bomb on 3, 4
Drop bomb on 3, 3
Drop bomb on 3, 3
Drop bomb on 3, 6
Drop bomb on 3, 6
Drop bomb on 3, 6
Drop bomb on 4, 6
28

但是,该算法仅在每一步之后至少有一个完美的下降点时才有效。可以构造没有完美落点的示例:

0 1 1 0
1 0 0 1
1 0 0 1
0 1 1 0

对于这些情况,我们可以修改算法,以便选择一个具有最大落点的最小选择的坐标,而不是一个完美的落点,然后计算每个选择的最小值。在上述情况下,所有具有值的像元都有两个最大下降点。例如,(0, 1)具有最大放置点(1, 1)(1, 2)。选择一个然后计算最小值将导致以下结果:

Drop bomb on 1, 1
Drop bomb on 2, 2
Drop bomb on 1, 2
Drop bomb on 2, 1
2

这几乎就是上面介绍的贪婪算法。
darksky 2013年

嗯,它也是一个贪婪的算法,但是我没有关注于拐角和边缘,而是定义了如何选择下一个下降点。使用5x7的示例正方形,很容易谈论1000x1000字段上的拐角,而不是那么多。如果您检查我的算法清除字段的顺序,则不是从外到内,而是从上到下/从左到右。
Tammo Freese 2013年

2

这是另一个想法:

首先,为面板上的每个空间分配一个权重,以了解通过在其中放置炸弹可以减少多少数字。因此,如果该空间具有非零数字,它将获得一个点;如果与其相邻的任何空间具有非零数字,则它将获得一个附加点。因此,如果有一个1000 x 1000的网格,则将权重分配给这100万个空间。

然后按重量对空间列表进行排序,并对重量最大的空间进行轰炸。可以这么说,这是我们最大的收获。

之后,更新其重量受炸弹影响的每个空间的重量。这将是您轰炸的空间,紧邻其的任何空间以及与其紧邻的任何空间。换句话说,任何可能通过轰炸将其值减小为零的空间,或者将相邻空间的值减小为零的空间。

然后,按重量对列表空间重新排序。由于轰炸只改变了一小部分空间的重量,因此您无需重新选择整个列表,只需在列表中四处移动即可。

炸毁新的最高重量的空间,然后重复该过程。

这样可以确保每次轰炸都会减少尽可能多的空间(基本上,它会命中尽可能少的已经为零的空间),因此这是最佳选择,除非它们可以成为重量关系。因此,当顶部重物绑平时,您可能需要进行一些向后跟踪。但是,只有最高重量的平局很重要,其他平局并不重要,因此希望它不会过多地回溯。

编辑: 下面的Mysticial的反例表明,实际上,无论权重如何,都不能保证这是最佳的。在某些情况下,在给定步骤中尽可能地减轻重量实际上会使剩余的炸弹散布得太远,以至于在第二步之后获得的累积减少量要比第一步中的贪婪选择少一些。我对结果对爆炸顺序不敏感的观念有些误解。他们对顺序不敏感,因为您可以进行任何系列的轰炸,并从头开始以不同顺序重播它们,并最终得到相同的结果板。但这并不是说您可以独立考虑每次轰炸。或者,至少,每次轰炸都必须以某种方式加以考虑,要考虑到其为随后的轰炸建立董事会的能力。


最初,由于字段的零值很少,所以大多数回溯权重都是9。
Lie Ryan

是的,这很不错,因为可能的权重范围不大(只有0到9)。
蒂姆·古德曼

我仍然不是100%地确定回溯的必要性...构建一个网格,其中一种贪婪轰炸要比另一种贪婪轰炸逊色。也许有一些一致的方法可以预测哪个更好。
蒂姆·古德曼

实际上,我看到Panic上校在他的回答中做到了这一点。一个贪婪的选择可能比另一个更好的原因是,一个让剩余的数字散布开来。
蒂姆·古德曼

3
10101010010100可能是一个反例,证明此方法不是最佳方法。这种方法需要3。它可以在第2做
Mysticial

1

好吧,假设我们为板的位置编号1、2,...,nx m。炸弹投掷的任何序列都可以由该组中的数字序列表示,其中数字可以重复。但是,无论您按什么顺序投下炸弹,在板上的效果都是相同的,因此实际上任何选择的炸弹投掷都可以表示为nxm编号列表,其中第一个数字代表放置在位置1的炸弹数量,第二个数字表示放置在位置2上的炸弹数量,以此类推。让我们将此nxm数字列表称为“键”。

您可以尝试首先计算由1次炸弹投掷导致的所有板状态,然后使用这些计算由2次炸弹投掷导致的所有板状态,依此类推,直到获得全零为止。但是在每一步,您都将使用上面定义的键来缓存状态,因此您可以将这些结果用于计算下一步(“动态编程”方法)。

但是根据n,m的大小以及网格中的数字,此方法的内存要求可能会过高。计算完N + 1的所有结果后,就可以丢弃N炸弹下落的所有结果,因此可以节省一些费用。当然,你不能在有它需要的成本缓存东西很多长-动态规划方法交易存储速度。


1
因为(如果我理解正确的话),这是有可能的。n =米 我需要10 ^ 6 int指针指向(10 ^ 6)^ 2 int单元格。我的桌子和桌子上的钥匙一样多。10 ^ 12怀疑我可以在32bit机器上分配这么多。
2013年

是的,我刚刚看到您对板的数量达到1000×1000的评论。因此,每个板的状态为一百万个整数,每个位置上的炸弹计数为一百万个整数。因此,对于您存储的每个板,您需要200万个整数,并且有很多可能的板...
Tim Goodman 2013年

我添加了使用不同方法的第二个答案。
Tim Goodman

1
是的 有点蛮力的方法,但是我想对于大板来说不是很实用。
蒂姆·古德曼

@Kostek,为什么这么低的估算?它更像是k ^(m * n)内存,其中k是电路板最初填充数量的限制。
Rotsor

1

如果您想要绝对最佳的解决方案来清洁电路板,则必须使用经典的回溯,但是如果矩阵很大,则寻找最佳解决方案将花费很多时间,如果您想要“可能的”最佳解决方案,则可以使用贪婪算法,如果您需要编写算法的帮助,我可以为您提供帮助

想到这是最好的方法。在此处制作另一个矩阵,在其中存储要删除的点,方法是将一个炸弹放到那里,然后选择具有最大点数的单元格,然后将炸弹放到那里,更新点矩阵并继续。例:

2 3 5 -> (2+(1*3)) (3+(1*5)) (5+(1*3))
1 3 2 -> (1+(1*4)) (3+(1*7)) (2+(1*4))
1 0 2 -> (1+(1*2)) (0+(1*5)) (2+(1*2))

值大于0的每个相邻像元的像元值+1


7
不得不使用经典回溯。你有证明吗?
Shahbaz 2013年

我不确定。这是我准备的比赛(从去年开始)。限制为1 <= n,m <= 1000(不知道是否大)。无论如何,您都需要确切的答案(类似于CERC竞赛等)。没有时间限制,也没有答案,比赛页面也没有解决方案。
2013年

以及其他所有算法都可以为您提供最佳解决方案,但是在您尝试所有这些方法(回溯)之前,您将不知道该解决方案是否是最佳解决方案
cosmin.danisor 2013年

2
您不需要使用回溯,因为它是您搜索的组合,而不是排列组合。投下炸弹的顺序并不重要
Luka Rahne 2013年

那么您可以尝试使用贪婪的变体。在每一步中创建一个新矩阵,每个点的值都等于其单元格的值+相邻单元格的值> 1,这样> 0会更好地选择放置下
一颗

1

蛮力!

我知道它效率不高,但是即使您找到了更快的算法,也可以始终对照此结果进行测试以了解其准确性。

使用一些递归,像这样:

void fn(tableState ts, currentlevel cl)
{
  // first check if ts is all zeros yet, if not:
  //
  // do a for loop to go through all cells of ts, 
  // for each cell do a bomb, and then
  // call: 
  // fn(ts, cl + 1);

}

您可以通过缓存来提高效率,如果不同的方式导致相同的结果,则不应重复相同的步骤。

详细说明:

如果轰炸单元格1,3,5与轰炸单元格5,3,1导致的结果相同,则对于这两种情况,您都不应再次执行所有后续步骤,仅1个就足够了,您应该将所有位置存储表状态并使用其结果。

表统计信息的哈希可用于进行快速比较。


1
  1. 永远不要炸弹边界(除非广场没有非边界邻居)
  2. 零角。
  3. 到零角时,对角线对角线(唯一的无边界邻居)的角点的下降值
  4. 这将创建新的角落。转到2

编辑:没有注意到Kostek提出了几乎相同的方法,所以现在我有更强的主张:如果选择要清除的角始终位于最外层,则它是最佳选择。

在OP的示例中:在5以外的任何位置上放2(作为1 + 1或2)不会导致碰到任何在5上会落下的正方形。因此,我们只需在5上放2(在左下角1放6)...

此后,只有一种方法可以清除(在左上角)原来是1(现在是0)的内容,也就是通过在B3上放0(像记号一样出色)。等等。

仅在清除整个A和E列以及1和7行之后,才开始清除更深的一层。

只考虑清除那些有意清除的对象,清除0个值角不会花费任何费用,并且简化了思考过程。

因为必须丢弃所有以这种方式投下的炸弹,并且这会导致空旷地带,所以这是最佳解决方案。


经过良好的睡眠,我意识到这是不正确的。考虑

  ABCDE    
1 01000
2 10000
3 00000
4 00000

我的方法是在B3和C2上投下炸弹,而在B2上投下就足够了


但这是最佳选择吗?
Mysticial

7
可以用2种方法炸毁新角(如果大多数角点包含所有4个值中的最低点)。哪个是最佳轰炸?
2013年

我在考虑类似的方法,当您遇到Kostek所述的情况时,便开始使用回溯...
Karoly Horvath 2013年

角落给您的炸弹投在对角线上的最小数量。但是将它们归零后,下一个边框图块不一定会具有明显的最佳点。不过,这是减少搜索空间的好方法。
尤金

如何选择在点击框中产生最高总计数的新对角线对角线?
法官Maygarden 2013年

1

这是我的解决方案。由于我没有时间,所以我不会在代码中写出来,但是我相信这每次都会产生最佳的移动次数-尽管我不确定查找的效率如何。炸弹要点。

首先,正如@Luka Rahne在其中一项评论中所述,炸弹的顺序并不重要-只是组合。

其次,正如许多其他人所述,从对角线对角线1-轰炸是最佳选择,因为它接触的点比对角线多。

这为我的算法版本奠定了基础:我们可以首先轰炸“ 1-off from corners”,这没关系(理论上),我们首先轰炸,因为这样可以简化以后的决策(在实践中)我们轰炸影响最大点的点,同时轰炸那些角。

让我们来定义点阻力是在板上的点与大多数非bombable点 + 最大的0的号码身边

不可炸弹的点可以定义为我们当前正在研究的董事会范围内不存在的点。

我还将定义4个边界来处理我们的范围:Top = 0,Left = 0,Bottom = k,right = j。(值开始)

最后,我将最佳炸弹定义为炸弹,这些炸弹会落在与阻力点相邻的点上,并且接触(1)最高阻力点和(2)可能的最大点数。

关于这种方法-很明显,我们是从外部进行工作。我们将能够同时使用4个“轰炸机”。

抵抗的第一点显然是我们的弯角。“越界”点不可炸弹(每个角点在范围外有5个点)。因此,我们首先将对角线炸开一个角。

算法:

  1. 找到4个最佳炸弹点。
  2. 如果炸弹点正在轰炸接触2个边界(即一个角)的抵抗点,则炸弹直至该点为0。否则,对每个炸弹进行轰炸,直到接触最佳炸弹点的抵抗点之一为0。
  3. 对于每个边界:if(sum(bound)== 0)提前边界

重复直到TOP = BOTTOM和LEFT = RIGHT

稍后我将尝试编写实际代码


1

您可以使用状态空间规划。例如,结合使用A *(或其变体之一)和f = g + h类似的启发式方法:

  • g:到目前为止投放的炸弹数量
  • h:网格的所有值之和除以9(这是最好的结果,这意味着我们有一个允许的启发式方法)

1

我也有28招。我使用了两个测试来确定最佳下一步:首先,此步产生最小的电路板总和。其次,对于相等的和,移动产生最大密度,定义为:

number-of-zeros / number-of-groups-of-zeros

这是Haskell。“解决方案板”显示了引擎的解决方案。您可以通过键入“主要”然后输入目标点,“最佳”作为推荐或“退出”退出来玩游戏。

输出:
* Main>求解板
[[4,4),(3,6),(3,3),(2,2),(2,2),(4,6),(4,6), (2,6),(3,2),(4,2),(2,6),(3,3),(4,3),(2,6),(4,2),(4 ,6),(4,6),(3,6),(2,6),(2,6),(2,4),(2,4),(2,6),(3,6 ),(4,2),(4,2),(4,2),(4,2)]

import Data.List
import Data.List.Split
import Data.Ord
import Data.Function(on)

board = [2,3,4,7,1,
         1,5,2,6,2,
         4,3,4,2,1,
         2,1,2,4,1,
         3,1,3,4,1,
         2,1,4,3,2,
         6,9,1,6,4]

n = 5
m = 7

updateBoard board pt =
  let x = fst pt
      y = snd pt
      precedingLines = replicate ((y-2) * n) 0
      bomb = concat $ replicate (if y == 1
                                    then 2
                                    else min 3 (m+2-y)) (replicate (x-2) 0 
                                                         ++ (if x == 1 
                                                                then [1,1]
                                                                else replicate (min 3 (n+2-x)) 1)
                                                                ++ replicate (n-(x+1)) 0)
  in zipWith (\a b -> max 0 (a-b)) board (precedingLines ++ bomb ++ repeat 0)

showBoard board = 
  let top = "   " ++ (concat $ map (\x -> show x ++ ".") [1..n]) ++ "\n"
      chunks = chunksOf n board
  in putStrLn (top ++ showBoard' chunks "" 1)
       where showBoard' []     str count = str
             showBoard' (x:xs) str count =
               showBoard' xs (str ++ show count ++ "." ++ show x ++ "\n") (count+1)

instances _ [] = 0
instances x (y:ys)
  | x == y    = 1 + instances x ys
  | otherwise = instances x ys

density a = 
  let numZeros = instances 0 a
      groupsOfZeros = filter (\x -> head x == 0) (group a)
  in if null groupsOfZeros then 0 else numZeros / fromIntegral (length groupsOfZeros)

boardDensity board = sum (map density (chunksOf n board))

moves = [(a,b) | a <- [2..n-1], b <- [2..m-1]]               

bestMove board = 
  let lowestSumMoves = take 1 $ groupBy ((==) `on` snd) 
                              $ sortBy (comparing snd) (map (\x -> (x, sum $ updateBoard board x)) (moves))
  in if null lowestSumMoves
        then (0,0)
        else let lowestSumMoves' = map (\x -> fst x) (head lowestSumMoves) 
             in fst $ head $ reverse $ sortBy (comparing snd) 
                (map (\x -> (x, boardDensity $ updateBoard board x)) (lowestSumMoves'))   

solve board = solve' board [] where
  solve' board result
    | sum board == 0 = result
    | otherwise      = 
        let best = bestMove board 
        in solve' (updateBoard board best) (result ++ [best])

main :: IO ()
main = mainLoop board where
  mainLoop board = do 
    putStrLn ""
    showBoard board
    putStr "Pt: "
    a <- getLine
    case a of 
      "quit"    -> do putStrLn ""
                      return ()
      "best"    -> do putStrLn (show $ bestMove board)
                      mainLoop board
      otherwise -> let ws = splitOn "," a
                       pt = (read (head ws), read (last ws))
                   in do mainLoop (updateBoard board pt)

1

这里似乎有一个非二分匹配的子结构。考虑以下实例:

0010000
1000100
0000001
1000000
0000001
1000100
0010000

这种情况下的最佳解决方案的大小为5,因为这是9个循环的顶点被其边缘最小覆盖的大小。

尤其是这种情况表明,一些人发表的线性编程放松是不准确的,行不通的,还有所有其他不好的事情。我很确定我可以减少“用尽可能少的边缘覆盖平面立方图的顶点”到您的问题,这使我怀疑是否有任何贪婪/爬山解决方案都可以使用。

在最坏的情况下,我看不到在多项式时间内解决此问题的方法。我可能没有看到非常聪明的二进制搜索和DP解决方案。

编辑:我看到比赛(http://deadline24.pl)与语言无关;他们向您发送了一堆输入文件,而您向其发送输出。因此,您不需要在最坏情况的多项式时间内运行的对象。特别要注意的是输入

输入中有很多小案例。然后是10x1000的情况,100x100的情况和1000x1000的情况。这三个大案例的表现都很好。水平相邻的条目通常具有相同的值。在相对强大的计算机上,我可以在短短几分钟内通过使用CPLEX进行强行解决所有情况。我对1000x1000感到幸运;LP松弛恰好有一个整体最优解。我的解决方案与.ans测试数据包中提供的文件一致。

我敢打赌,您可以以比直接看输入时更直接的方式使用输入的结构;似乎您可以重复删除第一行,两行或三行,直到您一无所有。(看起来,在1000x1000中,所有行都没有增加?我想这就是您的“ B部分”的来源吗?)


对。有时我只是跳过文本的“无关”部分。只是简单地了解一下想法等等。这次,它的级别基本上从容易变为困难:P无论如何,我知道您可以尝试进行具有“已知”输入集的启发式操作。另一方面,我只是认为,如果答案不是百分比,那么必须有一些算法可以在5小时内轻松执行。我发现所有内容都过于复杂。然后,当有人问起来源时,我会仔细阅读:)
abc

可以说,这要感谢许多人有一个很好的问题,但是怀疑它是否可以在多项式时间内完成。有趣的是,一个简单的约束如何将任务的级别从容易变为不可能。
2013年

@Kostek:对不起,如果我不清楚。我...很不擅长向受众介绍适当的解释。:)我下落不明?
tmyklebu

1

我想不出一种方法来计算实际数字,而不仅仅是用我最好的试探法来计算轰炸活动,并希望我能得到一个合理的结果。

因此,我的方法是为每个单元格计算轰炸效率指标,轰炸具有最高价值的单元格,....重复该过程,直到将所有内容弄平为止。一些人提倡使用简单的潜在损害(即得分从0到9)作为度量标准,但由于冲击高价值单元且不利用损害重叠而达不到要求。我会计算cell value - sum of all neighbouring cells,将任何正数重置为0并使用任何负数的绝对值。直观地,该指标应做出选择,以帮助最大程度地破坏高计数单元格上的重叠,而不是直接敲打它们。

下面的代码在28枚炸弹中完全摧毁了测试区域(请注意,使用潜在的损害作为度量标准会产生31!)。

using System;
using System.Collections.Generic;
using System.Linq;

namespace StackOverflow
{
  internal class Program
  {
    // store the battle field as flat array + dimensions
    private static int _width = 5;
    private static int _length = 7;
    private static int[] _field = new int[] {
        2, 3, 4, 7, 1,
        1, 5, 2, 6, 2,
        4, 3, 4, 2, 1,
        2, 1, 2, 4, 1,
        3, 1, 3, 4, 1,
        2, 1, 4, 3, 2,
        6, 9, 1, 6, 4
    };
    // this will store the devastation metric
    private static int[] _metric;

    // do the work
    private static void Main(string[] args)
    {
        int count = 0;

        while (_field.Sum() > 0)
        {
            Console.Out.WriteLine("Round {0}:", ++count);
            GetBlastPotential();
            int cell_to_bomb = FindBestBombingSite();
            PrintField(cell_to_bomb);
            Bomb(cell_to_bomb);
        }
        Console.Out.WriteLine("Done in {0} rounds", count);
    } 

    // convert 2D position to 1D index
    private static int Get1DCoord(int x, int y)
    {
        if ((x < 0) || (y < 0) || (x >= _width) || (y >= _length)) return -1;
        else
        {
            return (y * _width) + x;
        }
    }

    // Convert 1D index to 2D position
    private static void Get2DCoord(int n, out int x, out int y)
    {
        if ((n < 0) || (n >= _field.Length))
        {
            x = -1;
            y = -1;
        }
        else
        {
            x = n % _width;
            y = n / _width;
        }
    }

    // Compute a list of 1D indices for a cell neighbours
    private static List<int> GetNeighbours(int cell)
    {
        List<int> neighbours = new List<int>();
        int x, y;
        Get2DCoord(cell, out x, out y);
        if ((x >= 0) && (y >= 0))
        {
            List<int> tmp = new List<int>();
            tmp.Add(Get1DCoord(x - 1, y - 1));
            tmp.Add(Get1DCoord(x - 1, y));
            tmp.Add(Get1DCoord(x - 1, y + 1));
            tmp.Add(Get1DCoord(x, y - 1));
            tmp.Add(Get1DCoord(x, y + 1));
            tmp.Add(Get1DCoord(x + 1, y - 1));
            tmp.Add(Get1DCoord(x + 1, y));
            tmp.Add(Get1DCoord(x + 1, y + 1));

            // eliminate invalid coords - i.e. stuff past the edges
            foreach (int c in tmp) if (c >= 0) neighbours.Add(c);
        }
        return neighbours;
    }

    // Compute the devastation metric for each cell
    // Represent the Value of the cell minus the sum of all its neighbours
    private static void GetBlastPotential()
    {
        _metric = new int[_field.Length];
        for (int i = 0; i < _field.Length; i++)
        {
            _metric[i] = _field[i];
            List<int> neighbours = GetNeighbours(i);
            if (neighbours != null)
            {
                foreach (int j in neighbours) _metric[i] -= _field[j];
            }
        }
        for (int i = 0; i < _metric.Length; i++)
        {
            _metric[i] = (_metric[i] < 0) ? Math.Abs(_metric[i]) : 0;
        }
    }

    //// Compute the simple expected damage a bomb would score
    //private static void GetBlastPotential()
    //{
    //    _metric = new int[_field.Length];
    //    for (int i = 0; i < _field.Length; i++)
    //    {
    //        _metric[i] = (_field[i] > 0) ? 1 : 0;
    //        List<int> neighbours = GetNeighbours(i);
    //        if (neighbours != null)
    //        {
    //            foreach (int j in neighbours) _metric[i] += (_field[j] > 0) ? 1 : 0;
    //        }
    //    }            
    //}

    // Update the battle field upon dropping a bomb
    private static void Bomb(int cell)
    {
        List<int> neighbours = GetNeighbours(cell);
        foreach (int i in neighbours)
        {
            if (_field[i] > 0) _field[i]--;
        }
    }

    // Find the best bombing site - just return index of local maxima
    private static int FindBestBombingSite()
    {
        int max_idx = 0;
        int max_val = int.MinValue;
        for (int i = 0; i < _metric.Length; i++)
        {
            if (_metric[i] > max_val)
            {
                max_val = _metric[i];
                max_idx = i;
            }
        }
        return max_idx;
    }

    // Display the battle field on the console
    private static void PrintField(int cell)
    {
        for (int x = 0; x < _width; x++)
        {
            for (int y = 0; y < _length; y++)
            {
                int c = Get1DCoord(x, y);
                if (c == cell)
                    Console.Out.Write(string.Format("[{0}]", _field[c]).PadLeft(4));
                else
                    Console.Out.Write(string.Format(" {0} ", _field[c]).PadLeft(4));
            }
            Console.Out.Write(" || ");
            for (int y = 0; y < _length; y++)
            {
                int c = Get1DCoord(x, y);
                if (c == cell)
                    Console.Out.Write(string.Format("[{0}]", _metric[c]).PadLeft(4));
                else
                    Console.Out.Write(string.Format(" {0} ", _metric[c]).PadLeft(4));
            }
            Console.Out.WriteLine();
        }
        Console.Out.WriteLine();
    }           
  }
}

生成的轰炸模式如下所示(左侧的字段值,右侧的度量值)

Round 1:
  2   1   4   2   3   2   6  ||   7  16   8  10   4  18   6
  3   5   3   1   1   1   9  ||  11  18  18  21  17  28   5
  4  [2]  4   2   3   4   1  ||  19 [32] 21  20  17  24  22
  7   6   2   4   4   3   6  ||   8  17  20  14  16  22   8
  1   2   1   1   1   2   4  ||  14  15  14  11  13  16   7

Round 2:
  2   1   4   2   3   2   6  ||   5  13   6   9   4  18   6
  2   4   2   1   1  [1]  9  ||  10  15  17  19  17 [28]  5
  3   2   3   2   3   4   1  ||  16  24  18  17  17  24  22
  6   5   1   4   4   3   6  ||   7  14  19  12  16  22   8
  1   2   1   1   1   2   4  ||  12  12  12  10  13  16   7

Round 3:
  2   1   4   2   2   1   5  ||   5  13   6   7   3  15   5
  2   4   2   1   0   1   8  ||  10  15  17  16  14  20   2
  3  [2]  3   2   2   3   0  ||  16 [24] 18  15  16  21  21
  6   5   1   4   4   3   6  ||   7  14  19  11  14  19   6
  1   2   1   1   1   2   4  ||  12  12  12  10  13  16   7

Round 4:
  2   1   4   2   2   1   5  ||   3  10   4   6   3  15   5
  1   3   1   1   0   1   8  ||   9  12  16  14  14  20   2
  2   2   2   2   2  [3]  0  ||  13  16  15  12  16 [21] 21
  5   4   0   4   4   3   6  ||   6  11  18   9  14  19   6
  1   2   1   1   1   2   4  ||  10   9  10   9  13  16   7

Round 5:
  2   1   4   2   2   1   5  ||   3  10   4   6   2  13   3
  1   3   1   1   0  [0]  7  ||   9  12  16  13  12 [19]  2
  2   2   2   2   1   3   0  ||  13  16  15  10  14  15  17
  5   4   0   4   3   2   5  ||   6  11  18   7  13  17   6
  1   2   1   1   1   2   4  ||  10   9  10   8  11  13   5

Round 6:
  2   1   4   2   1   0   4  ||   3  10   4   5   2  11   2
  1   3   1   1   0   0   6  ||   9  12  16  11   8  13   0
  2   2   2   2   0   2   0  ||  13  16  15   9  14  14  15
  5   4  [0]  4   3   2   5  ||   6  11 [18]  6  11  15   5
  1   2   1   1   1   2   4  ||  10   9  10   8  11  13   5

Round 7:
  2   1   4   2   1   0   4  ||   3  10   4   5   2  11   2
  1   3   1   1   0   0   6  ||   8  10  13   9   7  13   0
  2  [1]  1   1   0   2   0  ||  11 [15] 12   8  12  14  15
  5   3   0   3   3   2   5  ||   3   8  10   3   8  15   5
  1   1   0   0   1   2   4  ||   8   8   7   7   9  13   5

Round 8:
  2   1   4   2   1   0   4  ||   1   7   2   4   2  11   2
  0   2   0   1   0   0   6  ||   7   7  12   7   7  13   0
  1   1   0   1   0   2   0  ||   8   8  10   6  12  14  15
  4   2   0   3   3  [2]  5  ||   2   6   8   2   8 [15]  5
  1   1   0   0   1   2   4  ||   6   6   6   7   9  13   5

Round 9:
  2   1   4   2   1   0   4  ||   1   7   2   4   2  11   2
  0   2   0   1   0   0   6  ||   7   7  12   7   6  12   0
  1   1   0   1   0  [1]  0  ||   8   8  10   5  10 [13] 13
  4   2   0   3   2   2   4  ||   2   6   8   0   6   9   3
  1   1   0   0   0   1   3  ||   6   6   6   5   8  10   4

Round 10:
  2   1   4   2   1   0   4  ||   1   7   2   4   2  10   1
  0   2  [0]  1   0   0   5  ||   7   7 [12]  7   6  11   0
  1   1   0   1   0   1   0  ||   8   8  10   4   8   9  10
  4   2   0   3   1   1   3  ||   2   6   8   0   6   8   3
  1   1   0   0   0   1   3  ||   6   6   6   4   6   7   2

Round 11:
  2   0   3   1   1   0   4  ||   0   6   0   3   0  10   1
  0   1   0   0   0  [0]  5  ||   4   5   5   5   3 [11]  0
  1   0   0   0   0   1   0  ||   6   8   6   4   6   9  10
  4   2   0   3   1   1   3  ||   1   5   6   0   5   8   3
  1   1   0   0   0   1   3  ||   6   6   6   4   6   7   2

Round 12:
  2   0   3   1   0   0   3  ||   0   6   0   2   1   7   1
  0   1   0   0   0   0   4  ||   4   5   5   4   1   7   0
  1   0   0   0   0  [0]  0  ||   6   8   6   4   5  [9]  8
  4   2   0   3   1   1   3  ||   1   5   6   0   4   7   2
  1   1   0   0   0   1   3  ||   6   6   6   4   6   7   2

Round 13:
  2   0   3   1   0   0   3  ||   0   6   0   2   1   6   0
  0   1   0   0   0   0   3  ||   4   5   5   4   1   6   0
  1  [0]  0   0   0   0   0  ||   6  [8]  6   3   3   5   5
  4   2   0   3   0   0   2  ||   1   5   6   0   4   6   2
  1   1   0   0   0   1   3  ||   6   6   6   3   4   4   0

Round 14:
  2   0   3   1   0  [0]  3  ||   0   5   0   2   1  [6]  0
  0   0   0   0   0   0   3  ||   2   5   4   4   1   6   0
  0   0   0   0   0   0   0  ||   4   4   4   3   3   5   5
  3   1   0   3   0   0   2  ||   0   4   5   0   4   6   2
  1   1   0   0   0   1   3  ||   4   4   5   3   4   4   0

Round 15:
  2   0   3   1   0   0   2  ||   0   5   0   2   1   4   0
  0   0   0   0   0   0   2  ||   2   5   4   4   1   4   0
  0   0   0   0   0   0   0  ||   4   4   4   3   3   4   4
  3   1   0   3   0  [0]  2  ||   0   4   5   0   4  [6]  2
  1   1   0   0   0   1   3  ||   4   4   5   3   4   4   0

Round 16:
  2  [0]  3   1   0   0   2  ||   0  [5]  0   2   1   4   0
  0   0   0   0   0   0   2  ||   2   5   4   4   1   4   0
  0   0   0   0   0   0   0  ||   4   4   4   3   3   3   3
  3   1   0   3   0   0   1  ||   0   4   5   0   3   3   1
  1   1   0   0   0   0   2  ||   4   4   5   3   3   3   0

Round 17:
  1   0   2   1   0   0   2  ||   0   3   0   1   1   4   0
  0   0   0   0   0   0   2  ||   1   3   3   3   1   4   0
  0   0   0   0   0   0   0  ||   4   4   4   3   3   3   3
  3   1  [0]  3   0   0   1  ||   0   4  [5]  0   3   3   1
  1   1   0   0   0   0   2  ||   4   4   5   3   3   3   0

Round 18:
  1   0   2   1   0   0   2  ||   0   3   0   1   1   4   0
  0   0   0   0   0   0   2  ||   1   3   3   3   1   4   0
  0   0   0   0   0   0   0  ||   3   3   2   2   2   3   3
  3  [0]  0   2   0   0   1  ||   0  [4]  2   0   2   3   1
  1   0   0   0   0   0   2  ||   2   4   2   2   2   3   0

Round 19:
  1   0   2   1   0  [0]  2  ||   0   3   0   1   1  [4]  0
  0   0   0   0   0   0   2  ||   1   3   3   3   1   4   0
  0   0   0   0   0   0   0  ||   2   2   2   2   2   3   3
  2   0   0   2   0   0   1  ||   0   2   2   0   2   3   1
  0   0   0   0   0   0   2  ||   2   2   2   2   2   3   0

Round 20:
  1  [0]  2   1   0   0   1  ||   0  [3]  0   1   1   2   0
  0   0   0   0   0   0   1  ||   1   3   3   3   1   2   0
  0   0   0   0   0   0   0  ||   2   2   2   2   2   2   2
  2   0   0   2   0   0   1  ||   0   2   2   0   2   3   1
  0   0   0   0   0   0   2  ||   2   2   2   2   2   3   0

Round 21:
  0   0   1   1   0   0   1  ||   0   1   0   0   1   2   0
  0   0   0   0   0   0   1  ||   0   1   2   2   1   2   0
  0   0   0   0   0   0   0  ||   2   2   2   2   2   2   2
  2   0   0   2   0  [0]  1  ||   0   2   2   0   2  [3]  1
  0   0   0   0   0   0   2  ||   2   2   2   2   2   3   0

Round 22:
  0   0   1   1   0   0   1  ||   0   1   0   0   1   2   0
  0   0   0   0   0   0   1  ||   0   1   2   2   1   2   0
 [0]  0   0   0   0   0   0  ||  [2]  2   2   2   2   1   1
  2   0   0   2   0   0   0  ||   0   2   2   0   2   1   1
  0   0   0   0   0   0   1  ||   2   2   2   2   2   1   0

Round 23:
  0   0   1   1   0   0   1  ||   0   1   0   0   1   2   0
  0   0  [0]  0   0   0   1  ||   0   1  [2]  2   1   2   0
  0   0   0   0   0   0   0  ||   1   1   2   2   2   1   1
  1   0   0   2   0   0   0  ||   0   1   2   0   2   1   1
  0   0   0   0   0   0   1  ||   1   1   2   2   2   1   0

Round 24:
  0   0   0   0   0   0   1  ||   0   0   0   0   0   2   0
  0   0   0   0   0   0   1  ||   0   0   0   0   0   2   0
  0   0  [0]  0   0   0   0  ||   1   1  [2]  2   2   1   1
  1   0   0   2   0   0   0  ||   0   1   2   0   2   1   1
  0   0   0   0   0   0   1  ||   1   1   2   2   2   1   0

Round 25:
  0   0   0   0   0  [0]  1  ||   0   0   0   0   0  [2]  0
  0   0   0   0   0   0   1  ||   0   0   0   0   0   2   0
  0   0   0   0   0   0   0  ||   1   1   1   1   1   1   1
  1   0   0   1   0   0   0  ||   0   1   1   0   1   1   1
  0   0   0   0   0   0   1  ||   1   1   1   1   1   1   0

Round 26:
  0   0   0   0   0   0   0  ||   0   0   0   0   0   0   0
  0   0   0   0   0   0   0  ||   0   0   0   0   0   0   0
 [0]  0   0   0   0   0   0  ||  [1]  1   1   1   1   0   0
  1   0   0   1   0   0   0  ||   0   1   1   0   1   1   1
  0   0   0   0   0   0   1  ||   1   1   1   1   1   1   0

Round 27:
  0   0   0   0   0   0   0  ||   0   0   0   0   0   0   0
  0   0   0   0   0   0   0  ||   0   0   0   0   0   0   0
  0   0  [0]  0   0   0   0  ||   0   0  [1]  1   1   0   0
  0   0   0   1   0   0   0  ||   0   0   1   0   1   1   1
  0   0   0   0   0   0   1  ||   0   0   1   1   1   1   0

Round 28:
  0   0   0   0   0   0   0  ||   0   0   0   0   0   0   0
  0   0   0   0   0   0   0  ||   0   0   0   0   0   0   0
  0   0   0   0   0   0   0  ||   0   0   0   0   0   0   0
  0   0   0   0   0  [0]  0  ||   0   0   0   0   0  [1]  1
  0   0   0   0   0   0   1  ||   0   0   0   0   0   1   0

Done in 28 rounds

1

这可以使用深度为O(3 ^(n))的树来解决。其中,n是所有平方的和。

首先考虑用O(9 ^ n)的树解决问题很简单,只需考虑所有可能的轰炸位置。有关示例,请参见Alfe的实现

接下来,我们可以进行自下而上的炸弹轰炸,并且仍能获得最小的轰炸模式。

  1. 从左下角开始。
  2. 用唯一有意义的剧本(向右上方)炸毁它。
  3. 向右移动一个方块。
  4. 当目标的值大于零时,请考虑2个有意义的游戏中的每一个(向上或向上并向右),将目标的值减小一个,并为每种可能性创建一个新分支。
  5. 向右移动另一个。
  6. 当目标的值大于零时,请考虑3个有意义的游戏(左上,上,右上),将目标的值减一,并为每种可能性建立新的分支。
  7. 重复步骤5和6,直到消除该行。
  8. 向上移动并重复步骤1至7,直到解决难题。

该算法是正确的,因为

  1. 有必要在某一点完成每一行。
  2. 要完成一行,总是需要在该行的上方,下方或内部播放。
  3. 选择最低未清除行上方的播放总是比该行上或该行下方的播放好或更好。

在实践中,该算法通常会比其理论上的最大值做得更好,因为它会定期轰炸邻居并减小搜索范围。如果我们假设每次轰炸都会减少4个额外目标的值,那么我们的算法将以O(3 ^(n / 4))或大约O(1.3 ^ n)的速度运行。

由于此算法仍然是指数算法,因此限制搜索深度是明智的。我们可以将允许的分支数量限制为某个数量X,一旦达到如此深的深度,我们便会强制算法选择迄今为止已确定的最佳路径(该路径在其最后一个分支中具有最小的总板数之和) )。那么我们的算法可以保证在O(3 ^ X)的时间内运行,但不能保证获得正确的答案。但是,如果值得在增加的计算量和更好的答案之间进行权衡,那么我们总是可以增加X并进行经验测试。


1

评估函数,总和:

int f (int ** matrix, int width, int height, int x, int y)
{
    int m[3][3] = { 0 };

    m[1][1] = matrix[x][y];
    if (x > 0) m[0][1] = matrix[x-1][y];
    if (x < width-1) m[2][1] = matrix[x+1][y];

    if (y > 0)
    {
        m[1][0] = matrix[x][y-1];
        if (x > 0) m[0][0] = matrix[x-1][y-1];
        if (x < width-1) m[2][0] = matrix[x+1][y-1];
    }

    if (y < height-1)
    {
        m[1][2] = matrix[x][y+1];
        if (x > 0) m[0][2] = matrix[x-1][y+1];
        if (x < width-1) m[2][2] = matrix[x+1][y+1];
    }

    return m[0][0]+m[0][1]+m[0][2]+m[1][0]+m[1][1]+m[1][2]+m[2][0]+m[2][1]+m[2][2];
}

目标函数:

Point bestState (int ** matrix, int width, int height)
{
    Point p = new Point(0,0);
    int bestScore = 0;
    int b = 0;

    for (int i=0; i<width; i++)
        for (int j=0; j<height; j++)
        {
            b = f(matrix,width,height,i,j);

            if (b > bestScore)
            {
                bestScore = best;
                p = new Point(i,j);
            }
        }

    retunr p;
}

销毁功能:

void destroy (int ** matrix, int width, int height, Point p)
{
    int x = p.x;
    int y = p.y;

    if(matrix[x][y] > 0) matrix[x][y]--;
    if (x > 0) if(matrix[x-1][y] > 0) matrix[x-1][y]--;
    if (x < width-1) if(matrix[x+1][y] > 0) matrix[x+1][y]--;

    if (y > 0)
    {
        if(matrix[x][y-1] > 0) matrix[x][y-1]--;
        if (x > 0) if(matrix[x-1][y-1] > 0) matrix[x-1][y-1]--;
        if (x < width-1) if(matrix[x+1][y-1] > 0) matrix[x+1][y-1]--;
    }

    if (y < height-1)
    {
        if(matrix[x][y] > 0) matrix[x][y+1]--;
        if (x > 0) if(matrix[x-1][y+1] > 0) matrix[x-1][y+1]--;
        if (x < width-1) if(matrix[x+1][y+1] > 0) matrix[x+1][y+1]--;
    }
}

目标函数:

bool isGoal (int ** matrix, int width, int height)
{
    for (int i=0; i<width; i++)
        for (int j=0; j<height; j++)
            if (matrix[i][j] > 0)
                return false;
    return true;
}

线性最大化函数:

void solve (int ** matrix, int width, int height)
{
    while (!isGoal(matrix,width,height))
    {
        destroy(matrix,width,height, bestState(matrix,width,height));
    }
}

这不是最佳的,但是可以通过找到更好的评估函数来优化。

..但是考虑到这个问题,我想到的主要问题之一是在某个时候将数字遗弃在零的中间,所以我会采取另一种方法..将最小值控制为零,然后尝试尽可能避开零,从而导致最小现存值的一般最小化


0

所有这些问题归结为计算编辑距离。只需计算给定矩阵和零矩阵之间的Levenshtein距离的变体,然后使用动态编程存储中间数组之间的距离,即可用炸弹取代编辑内容。我建议使用矩阵的哈希作为键。在伪Python中:

memo = {}

def bomb(matrix,i,j):
    # bomb matrix at i,j

def bombsRequired(matrix,i,j):
    # bombs required to zero matrix[i,j]

def distance(m1, i, len1, m2, j, len2):
    key = hash(m1)
    if memo[key] != None: 
        return memo[key]

    if len1 == 0: return len2
    if len2 == 0: return len1

    cost = 0
    if m1 != m2: cost = m1[i,j]
    m = bomb(m1,i,j)
    dist = distance(str1,i+1,len1-1,str2,j+1,len2-1)+cost)
    memo[key] = dist
    return dist

0

这是第一个问题的答案。我没有注意到他更改了参数。

创建所有目标的列表。根据受墨滴影响的正值数量(自身和所有邻居)为目标分配一个值。最高值将是9。

按受影响目标的数量(降序)对目标进行排序,对每个受影响目标的总和进行次降序排序。

将炸弹放到排名最高的目标上,然后重新计算目标并重复直到所有目标值均为零。

同意,这并不总是最佳的。例如,

100011
011100
011100
011100
000000
100011

这种方法需要5枚炸弹才能清除。最佳情况是,您可以在4中完成它。不过,该死的还算不错,并且没有回溯。对于大多数情况,它是最佳的,或者非常接近。

使用原始问题编号,此方法可解决28枚炸弹。

添加代码以演示此方法(使用带有按钮的表单):

         private void button1_Click(object sender, EventArgs e)
    {
        int[,] matrix = new int[10, 10] {{5, 20, 7, 1, 9, 8, 19, 16, 11, 3}, 
                                         {17, 8, 15, 17, 12, 4, 5, 16, 8, 18},
                                         { 4, 19, 12, 11, 9, 7, 4, 15, 14, 6},
                                         { 17, 20, 4, 9, 19, 8, 17, 2, 10, 8},
                                         { 3, 9, 10, 13, 8, 9, 12, 12, 6, 18}, 
                                         {16, 16, 2, 10, 7, 12, 17, 11, 4, 15},
                                         { 11, 1, 15, 1, 5, 11, 3, 12, 8, 3},
                                         { 7, 11, 16, 19, 17, 11, 20, 2, 5, 19},
                                         { 5, 18, 2, 17, 7, 14, 19, 11, 1, 6},
                                         { 13, 20, 8, 4, 15, 10, 19, 5, 11, 12}};


        int value = 0;
        List<Target> Targets = GetTargets(matrix);
        while (Targets.Count > 0)
        {
            BombTarget(ref matrix, Targets[0]);
            value += 1;
            Targets = GetTargets(matrix);
        }
        Console.WriteLine( value);
        MessageBox.Show("done: " + value);
    }

    private static void BombTarget(ref int[,] matrix, Target t)
    {
        for (int a = t.x - 1; a <= t.x + 1; a++)
        {
            for (int b = t.y - 1; b <= t.y + 1; b++)
            {
                if (a >= 0 && a <= matrix.GetUpperBound(0))
                {
                    if (b >= 0 && b <= matrix.GetUpperBound(1))
                    {
                        if (matrix[a, b] > 0)
                        {
                            matrix[a, b] -= 1;
                        }
                    }
                }
            }
        }
        Console.WriteLine("Dropped bomb on " + t.x + "," + t.y);
    }

    private static List<Target> GetTargets(int[,] matrix)
    {
        List<Target> Targets = new List<Target>();
        int width = matrix.GetUpperBound(0);
        int height = matrix.GetUpperBound(1);
        for (int x = 0; x <= width; x++)
        {
            for (int y = 0; y <= height; y++)
            {
                Target t = new Target();
                t.x = x;
                t.y = y;
                SetTargetValue(matrix, ref t);
                if (t.value > 0) Targets.Add(t);
            }
        }
        Targets = Targets.OrderByDescending(x => x.value).ThenByDescending( x => x.sum).ToList();
        return Targets;
    }

    private static void SetTargetValue(int[,] matrix, ref Target t)
    {
        for (int a = t.x - 1; a <= t.x + 1; a++)
        {
            for (int b = t.y - 1; b <= t.y + 1; b++)
            {
                if (a >= 0 && a <= matrix.GetUpperBound(0))
                {
                    if (b >= 0 && b <= matrix.GetUpperBound(1))
                    {
                        if (matrix[ a, b] > 0)
                        {
                            t.value += 1;
                            t.sum += matrix[a,b];
                        }

                    }
                }
            }
        }

    }

您需要的课程:

        class Target
    {
        public int value;
        public int sum;
        public int x;
        public int y;
    }

1
不是最佳的。反例:09090这种方法需要18枚炸弹。它可以在9
。– Mysticial

@Mysticial您没有完全阅读答案。由于该算法基于受影响的非零字段的数量,因此该算法将轰炸中间的零并在9滴内完成。9的高值是因为最多有8个邻居及其自身。
安东尼(Queen)

那么怎么样10101010010100
Mysticial 2013年

@Mysticial对于第一个,第一个零,然后最后一个零将被命中。这将是两滴。这是因为每次投下炸弹都会重新计算下一个最佳目标。在最后一个示例中,中间零再次出现。一滴。
安东尼·皇后

1
@AnthonyQueen:这行不通。请参阅chat.stackoverflow.com/transcript/message/8224273#8224273作为我的反例。
nneonneo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.