我有一个nxm个由非负整数组成的矩阵。例如:
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:我相信当我们有几个相同的情况需要最少的炸弹来清除上面的线时,我们会选择在“左侧”使用最多炸弹的一个。还有什么证据是正确的吗?
对于更新后的问题,简单的贪心算法可以得到最优结果。
向单元格A[1,1]投掷A[0,0]炸弹,然后向单元格A[2,1]投掷A[1,0]炸弹,并继续向下此过程。要清除左下角,向单元格A[n -2,1]投掷max(A[n -1,0], A[n -2,0], A[n -3,0])炸弹。这将完全清除前3列。
用同样的方法清除第3、4、5列,然后是第6、7、8列,等等。
不幸的是,这并不能帮助找到最初问题的解决方案。
“更大”的问题(没有“非增加”约束)可能被证明是np困难的。这是证明的草图。
假设我们有一个度为3的平面图形。我们来求这个图的最小顶点覆盖。根据维基百科的文章,这个问题对于3次以下的平面图形是np困难的。这可以通过平面3SAT的简化来证明。平面3SAT的硬度由3SAT降低而成。这两个证明都在Erik Demaine教授最近的“算法下界”讲座(第7和第9讲)中提出。
如果我们分割原始图的一些边(图中左边的图),每条边都有偶数个额外的节点,结果图(图中右边的图)应该对原始顶点具有完全相同的最小顶点覆盖。这样的转换允许将图顶点对齐到网格上的任意位置。
如果我们将图顶点只放置在偶数行和列上(这样就不会有两条边与一个顶点形成锐角),在有边的地方插入“1”,在其他网格位置插入“0”,我们可以使用原始问题的任何解决方案来找到最小顶点覆盖。
蛮力!
我知道它效率不高,但即使你找到了一个更快的算法,你也可以对这个结果进行测试,以了解它有多准确。
使用一些递归,像这样:
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就足够了,您应该将所有表状态存储在某个地方并使用其结果。
表统计信息的散列可以用于快速比较。
这将是一个贪婪的方法:
计算一个阶为n X m的“score”矩阵,其中score[i][j]是如果位置(i,j)被炸毁,则矩阵中各点的总扣除额。(一个点的最高分数是9分,最低分数是0分)
逐行移动,找到并选择第一个得分最高的位置(例如(i,j))。
炸弹(i, j)。增加炸弹数量。
如果原矩阵的所有元素都不为零,则转到1。
但我怀疑这是否是最佳解决方案。
编辑:
我上面提到的贪心方法,虽然有效,但很可能不能给我们最优的解决方案。所以我想应该添加一些DP的元素。
我想我们可以同意,在任何时候,具有最高“分数”(分数[I][j] =总扣分,如果(I,j)被炸)的位置之一必须被瞄准。从这个假设开始,下面是新的方法:
NumOfBombs(M):(返回所需的最小炸弹数量)
给定一个矩阵M (n X M),如果M中的所有元素都为0,则返回0。
计算“分数”矩阵M。
设k个不同的位置P1 P2…Pk (1 <= k <= n*m),为m中得分最高的位置。
return (1 + min(NumOfBombs(M1), NumOfBombs(M2),…, NumOfBombs(Mk))
M1, M2,……,Mk是我们轰炸位置P1, P2,…, Pk。
此外,如果我们想在此基础上破坏位置的顺序,我们必须跟踪“min”的结果。
这是部分答案,我试图找到一个下界和上界,可能是炸弹的数量。
在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个炸弹确实是一个最优解。
当贪心和我上面提到的寻找最小界的方法不收敛时,我猜你必须回去检查所有的组合。
求下界的算法如下:
选一个数值最大的元素,命名为P。
将所有距离P和P本身两步远的单元格标记为不可拾取。
将P添加到最小值列表中。
重复步骤1,直到所有单元格都不可拾取。
对最小值列表求和得到下界。
这是我的解决方案。由于时间有限,我不会用代码写出来,但我相信这应该每次都能产生最优的移动数量——尽管我不确定它在寻找要轰炸的点时是否有效。
首先,正如@Luka Rahne在一条评论中所说的,你轰炸的顺序并不重要,重要的是组合。
其次,正如许多人所说的那样,从角的对角线上轰炸1是最优的,因为它接触的点比角多。
这就生成了我的算法版本的基础:
我们可以在第一个或最后一个炸掉拐角的1-off,这没有关系(理论上)
我们首先破坏这些,因为它可以让后面的决定更容易(在实践中)
我们轰炸影响最大的点,同时轰炸那些角落。
让我们将阻力点定义为棋盘上具有最多不可炸点+周围0数量最多的点
非爆炸点可以定义为在我们正在研究的黑板的当前范围内不存在的点。
我还将定义4个处理范围的边界:
上=0,左=0,下=k,右=j。
(起始值)
最后,我将最优炸弹定义为投掷在与阻力点相邻的点上的炸弹,并接触(1)最高值的阻力点和(2)可能的最大数量的点。
关于方法,很明显我们正在从外到内的工作。我们将能够同时与4架“轰炸机”一起工作。
第一个阻力点显然是我们的弯道。“边界外”的点是不可轰炸的(每个角落的范围外都有5个点)。所以我们先在对角线上炸一个角。
算法:
找到4个最佳炸弹点。
如果一个炸弹点正在轰炸一个接触2个边界(即一个角)的阻力点,则一直轰炸到该点为0。否则,逐个轰炸,直到其中一个触及最佳轰炸点的阻力点为0。
对于每个边界:
如果(sum(bound)==0)前进界
重复以上步骤,直到上=下,左=右
稍后我将尝试编写实际代码