我有一个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:我相信当我们有几个相同的情况需要最少的炸弹来清除上面的线时,我们会选择在“左侧”使用最多炸弹的一个。还有什么证据是正确的吗?
永远不要轰炸边界(除非正方形没有边界以外的邻居)
零角落。
到零角,将对角线上一个正方形的角的值降低(唯一的非边界邻居)
这会产生新的角落。见第2节
编辑:没有注意到Kostek提出了几乎相同的方法,所以现在我提出了更强烈的主张:
如果要清除的角总是选择在最外层,那么它是最优的。
在OP的例子中:在除5之外的任何地方掉落2(1+1或2)并不会导致掉落5所能击中的任何方块。所以我们必须在5上加上2(在左下角加上6…)
在这之后,只有一种方法可以清除(在左上角)角落里原本是1(现在是0)的东西,那就是在B3上删除0(类似excel的符号)。
等等。
只有在清除了整个A和E列以及1和7行之后,才开始更深一层的清理。
考虑只清除那些故意清除的角落,清除0值的角落不需要花费任何成本,并且简化了思考。
因为所有以这种方式投掷的炸弹都必须被投掷,并且这将导致清除战场,这是最佳解决方案。
睡了一觉后,我意识到这不是真的。
考虑
ABCDE
1 01000
2 10000
3 00000
4 00000
我的方法是在B3和C2上投放炸弹,而在B2上投放炸弹就足够了
这是部分答案,我试图找到一个下界和上界,可能是炸弹的数量。
在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,直到所有单元格都不可拾取。
对最小值列表求和得到下界。
这将是一个贪婪的方法:
计算一个阶为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核的场,并检查哪里的和更强。还有炸弹…一直这样做,直到场地变平。这个文件的答案是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');
这是我的解决方案。由于时间有限,我不会用代码写出来,但我相信这应该每次都能产生最优的移动数量——尽管我不确定它在寻找要轰炸的点时是否有效。
首先,正如@Luka Rahne在一条评论中所说的,你轰炸的顺序并不重要,重要的是组合。
其次,正如许多人所说的那样,从角的对角线上轰炸1是最优的,因为它接触的点比角多。
这就生成了我的算法版本的基础:
我们可以在第一个或最后一个炸掉拐角的1-off,这没有关系(理论上)
我们首先破坏这些,因为它可以让后面的决定更容易(在实践中)
我们轰炸影响最大的点,同时轰炸那些角落。
让我们将阻力点定义为棋盘上具有最多不可炸点+周围0数量最多的点
非爆炸点可以定义为在我们正在研究的黑板的当前范围内不存在的点。
我还将定义4个处理范围的边界:
上=0,左=0,下=k,右=j。
(起始值)
最后,我将最优炸弹定义为投掷在与阻力点相邻的点上的炸弹,并接触(1)最高值的阻力点和(2)可能的最大数量的点。
关于方法,很明显我们正在从外到内的工作。我们将能够同时与4架“轰炸机”一起工作。
第一个阻力点显然是我们的弯道。“边界外”的点是不可轰炸的(每个角落的范围外都有5个点)。所以我们先在对角线上炸一个角。
算法:
找到4个最佳炸弹点。
如果一个炸弹点正在轰炸一个接触2个边界(即一个角)的阻力点,则一直轰炸到该点为0。否则,逐个轰炸,直到其中一个触及最佳轰炸点的阻力点为0。
对于每个边界:
如果(sum(bound)==0)前进界
重复以上步骤,直到上=下,左=右
稍后我将尝试编写实际代码
这是一个广度搜索,通过这个“迷宫”的位置寻找最短路径(一系列轰炸)。不,我不能证明没有更快的算法,抱歉。
#!/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))