我有一个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:我相信当我们有几个相同的情况需要最少的炸弹来清除上面的线时,我们会选择在“左侧”使用最多炸弹的一个。还有什么证据是正确的吗?


当前回答

这是一个广度搜索,通过这个“迷宫”的位置寻找最短路径(一系列轰炸)。不,我不能证明没有更快的算法,抱歉。

#!/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))

其他回答

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

正如评论中指出的那样,它在2D中会失败。但也许你可以改进它。

1 d: 如果至少有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任务。(因为你需要使它为空)。下降。别忘了最后一排。

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

创建一个所有目标的列表。根据掉落物品(掉落物品本身和所有邻居)影响的正数值的数量为目标分配一个值。最高值是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;
    }

如果你想要绝对最优解来清理棋盘,你将不得不使用经典的回溯,但如果矩阵非常大,它将需要很长时间才能找到最佳解,如果你想要一个“可能的”最优解,你可以使用贪婪算法,如果你需要帮助写算法,我可以帮助你

现在想想,这是最好的办法。在那里制作另一个矩阵,存储通过投掷炸弹而移除的点,然后选择点数最多的单元格,并在那里投掷炸弹更新点数矩阵,然后继续。例子:

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

由于时间不够,我不得不停留在部分解决方案上,但希望即使是这个部分解决方案也能提供解决这个问题的潜在方法的一些见解。

当面对一个困难的问题时,我喜欢想出一些简单的问题来培养对问题空间的直觉。这里,我采取的第一步是将这个二维问题简化为一维问题。考虑一行字:

0 4 2 1 3 0 1

不管怎样,你知道你需要在4点附近炸4次才能把它降到0。因为左边是一个较低的数字,所以轰炸0或4比轰炸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

从一个需要以某种方式下降的数字开始是一个很有吸引力的想法,因为它突然变得可以找到一个解,就像一些人声称的那样,至少和所有其他解一样好。

The next step up in complexity where this search of at least as good is still feasible is on the edge of the board. It is clear to me that there is never any strict benefit to bomb the outer edge; you're better off bombing the spot one in and getting three other spaces for free. Given this, we can say that bombing the ring one inside of the edge is at least as good as bombing the edge. Moreover, we can combine this with the intuition that bombing the right one inside of the edge is actually the only way to get edge spaces down to 0. Even more, it is trivially simple to figure out the optimal strategy (in that it is at least as good as any other strategy) to get corner numbers down to 0. We put this all together and can get much closer to a solution in the 2-D space.

根据对角子的观察,我们可以肯定地说,我们知道从任何起始棋盘到所有角子都是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

The flaw in this approach becomes very obvious in the final two bombings. It is clear, given that the only bomb sites that reduce the 4 figure in the first column in the second row are the first x and the y. The final two bombings are clearly inferior to just bombing the first x, which would have done the exact same (with regard to the first spot in the top row, which we have no other way of clearing). Since we have demonstrated that our current strategy is suboptimal, a modification in strategy is clearly needed.

在这一点上,我可以退一步,只关注一个角落。让我们考虑一下这个问题:

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

It is clear the only way to get the spaces with 4 down to zero are to bomb some combination of x, y, and z. With some acrobatics in my mind, I'm fairly sure the optimal solution is to bomb x three times and then a then b. Now it's a matter of figuring out how I reached that solution and if it reveals any intuition we can use to even solve this local problem. I notice that there's no bombing of y and z spaces. Attempting to find a corner where bombing those spaces makes sense yields a corner that looks like this:

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

对于这个问题,我很清楚,最优解决方案是轰炸y 5次,z 5次。让我们更进一步。

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

这里,最优解决方案是轰炸a和b 6次,然后x 4次。

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

希望能继续!

没有必要将问题转化为线性子问题。

相反,使用简单的贪婪启发式,即从最大的角落开始轰炸角落。

在给定的例子中,有四个角,{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次爆炸消除3 0 2。现在,我们有两个新的角,在第四行{2,1}。我们炸了那些,再炸3次。我们现在已经将矩阵化简为4 * 4。有一个角落,左上角。我们搞砸了。现在我们还有两个角,{5,3}。因为5是最大的角,我们首先轰炸5个角,然后最后轰炸另一个角的3。总数是13+3+3+1+5+3 = 28。