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


当前回答

这是我的解决方案。由于时间有限,我不会用代码写出来,但我相信这应该每次都能产生最优的移动数量——尽管我不确定它在寻找要轰炸的点时是否有效。

首先,正如@Luka Rahne在一条评论中所说的,你轰炸的顺序并不重要,重要的是组合。

其次,正如许多人所说的那样,从角的对角线上轰炸1是最优的,因为它接触的点比角多。

这就生成了我的算法版本的基础: 我们可以在第一个或最后一个炸掉拐角的1-off,这没有关系(理论上) 我们首先破坏这些,因为它可以让后面的决定更容易(在实践中) 我们轰炸影响最大的点,同时轰炸那些角落。

让我们将阻力点定义为棋盘上具有最多不可炸点+周围0数量最多的点

非爆炸点可以定义为在我们正在研究的黑板的当前范围内不存在的点。

我还将定义4个处理范围的边界: 上=0,左=0,下=k,右=j。 (起始值)

最后,我将最优炸弹定义为投掷在与阻力点相邻的点上的炸弹,并接触(1)最高值的阻力点和(2)可能的最大数量的点。

关于方法,很明显我们正在从外到内的工作。我们将能够同时与4架“轰炸机”一起工作。

第一个阻力点显然是我们的弯道。“边界外”的点是不可轰炸的(每个角落的范围外都有5个点)。所以我们先在对角线上炸一个角。

算法:

找到4个最佳炸弹点。 如果一个炸弹点正在轰炸一个接触2个边界(即一个角)的阻力点,则一直轰炸到该点为0。否则,逐个轰炸,直到其中一个触及最佳轰炸点的阻力点为0。 对于每个边界: 如果(sum(bound)==0)前进界

重复以上步骤,直到上=下,左=右

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

其他回答

这是另一个想法:

让我们先给黑板上的每个空格分配一个权重,计算在那里扔炸弹会减少多少数字。如果这个空间有一个非零数,它就得到一个点,如果它的相邻空间有一个非零数,它就得到一个额外的点。如果这是一个1000 * 1000的网格,我们为这100万个空间中的每一个都分配了权重。

然后根据权重对列表中的空格进行排序,并轰炸权重最高的空格。可以这么说,这是我们最大的收获。

在此之后,更新每个空间的重量是受炸弹的影响。这是你轰炸的空间,和它相邻的空间,以及它们相邻的空间。换句话说,任何空间的价值都可能因为爆炸而减少为零,或者相邻空间的价值减少为零。

然后,根据权重重新排序列表空间。由于轰炸只改变了一小部分空间的权重,因此不需要使用整个列表,只需在列表中移动这些空间。

轰炸新的最高权重空间,并重复上述步骤。

这保证了每次轰炸都能减少尽可能多的空格(基本上,它会击中尽可能少的已经为零的空格),所以这是最优的,除非它们的权重是相同的。所以你可能需要做一些回溯跟踪,当有一个平局的顶部重量。不过,只有最高重量的领带重要,其他领带不重要,所以希望没有太多的回溯。

Edit: Mysticial's counterexample below demonstrates that in fact this isn't guaranteed to be optimal, regardless of ties in weights. In some cases reducing the weight as much as possible in a given step actually leaves the remaining bombs too spread out to achieve as high a cummulative reduction after the second step as you could have with a slightly less greedy choice in the first step. I was somewhat mislead by the notion that the results are insensitive to the order of bombings. They are insensitive to the order in that you could take any series of bombings and replay them from the start in a different order and end up with the same resulting board. But it doesn't follow from that that you can consider each bombing independently. Or, at least, each bombing must be considered in a way that takes into account how well it sets up the board for subsequent bombings.

你可以使用状态空间规划。 例如,使用A*(或其变体之一)加上启发式f = g + h,如下所示:

G:到目前为止投下的炸弹数量 H:网格中所有值的总和除以9(这是最好的结果,意味着我们有一个可接受的启发式)

这里有一个解决方案,推广良好的性质的角。

让我们假设我们可以为给定的字段找到一个完美的落点,也就是说,一个减少其中值的最佳方法。然后,为了找到最少的炸弹数量,一个算法的初稿可能是(代码是从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)的放置点A比(x, y)的放置点b更好,如果它减少了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

单元格(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):它减少了(2,2)中的值和(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

到目前为止,一些答案给出了指数时间,一些涉及动态规划。我怀疑这些是否有必要。

我的解是O(mnS)其中m和n是板子的维度,S是所有整数的和。这个想法相当野蛮:找到每次可以杀死最多的位置,并在0处终止。

对于给定的棋盘,它给出28步棋,并且在每次落子后打印出棋盘。

完整的,不言自明的代码:

import java.util.Arrays;

public class BombMinDrops {

    private static final int[][] 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}};
    private static final int ROWS = BOARD.length;
    private static final int COLS = BOARD[0].length;
    private static int remaining = 0;
    private static int dropCount = 0;
    static {
        for (int i = 0; i < ROWS; i++) {
            for (int j = 0; j < COLS; j++) {
                remaining = remaining + BOARD[i][j];
            }
        }
    }

    private static class Point {
        int x, y;
        int kills;

        Point(int x, int y, int kills) {
            this.x = x;
            this.y = y;
            this.kills = kills;
        }

        @Override
        public String toString() {
            return dropCount + "th drop at [" + x + ", " + y + "] , killed " + kills;
        }
    }

    private static int countPossibleKills(int x, int y) {
        int count = 0;
        for (int row = x - 1; row <= x + 1; row++) {
            for (int col = y - 1; col <= y + 1; col++) {
                try {
                    if (BOARD[row][col] > 0) count++;
                } catch (ArrayIndexOutOfBoundsException ex) {/*ignore*/}
            }
        }

        return count;
    }

    private static void drop(Point here) {
        for (int row = here.x - 1; row <= here.x + 1; row++) {
            for (int col = here.y - 1; col <= here.y + 1; col++) {
                try {
                    if (BOARD[row][col] > 0) BOARD[row][col]--;
                } catch (ArrayIndexOutOfBoundsException ex) {/*ignore*/}
            }
        }

        dropCount++;
        remaining = remaining - here.kills;
        print(here);
    }

    public static void solve() {
        while (remaining > 0) {
            Point dropWithMaxKills = new Point(-1, -1, -1);
            for (int i = 0; i < ROWS; i++) {
                for (int j = 0; j < COLS; j++) {
                    int possibleKills = countPossibleKills(i, j);
                    if (possibleKills > dropWithMaxKills.kills) {
                        dropWithMaxKills = new Point(i, j, possibleKills);
                    }
                }
            }

            drop(dropWithMaxKills);
        }

        System.out.println("Total dropped: " + dropCount);
    }

    private static void print(Point drop) {
        System.out.println(drop.toString());
        for (int[] row : BOARD) {
            System.out.println(Arrays.toString(row));
        }

        System.out.println();
    }

    public static void main(String[] args) {
        solve();
    }

}

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

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

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