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


当前回答

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

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

其他回答

Pólya说:“如果你不能解决一个问题,那么有一个更容易解决的问题:找到它。”

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

给定11 11,贪婪算法对先炸毁哪个单元格无关。当然,中心单元格更好——它一次将所有三个单元格归零。这就提出了一种新的算法a,“炸弹最小化剩余的总和”。这个算法什么时候会出错?

给定1 1 2 11 1,算法A在轰炸第2,第3或第4单元格之间是无所谓的。但是轰炸第二个单元格,留下0 0 11 11比轰炸第三个单元格,留下10 10 10 10 1好。如何解决这个问题?轰炸第三个单元格的问题是,左边的功和右边的功必须分开做。

“炸弹使剩余的总和最小化,但使左边(我们轰炸的地方)的最小值和右边的最小值最大化”如何?叫这个算法b,这个算法什么时候出错?


编辑:在阅读了评论之后,我同意一个更有趣的问题将是改变一维问题,使其两端连接起来。很乐意看到这方面的进展。

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

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

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

我也有28招。我使用了两个测试来确定最佳下一步:第一个是产生最小棋盘和的一步。其次,对于相等的和,产生最大密度的移动,定义为:

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

我是哈斯克尔。“解决板”显示引擎的解决方案。你可以通过输入“main”来玩游戏,然后输入目标点,“best”作为推荐,或者“quit”退出。

输出: *主>解决板 [(4, 4),(3、6),(3),(2,2),(2,2),(4、6)(4、6),(2,6),(2),(4,2)(2,6),(3),(4,3)(2,6)(4,2)(4、6)(4、6),(3、6),(2,6)(2,6)(2、4)(2、4)(2,6),(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)

蛮力!

我知道它效率不高,但即使你找到了一个更快的算法,你也可以对这个结果进行测试,以了解它有多准确。

使用一些递归,像这样:

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就足够了,您应该将所有表状态存储在某个地方并使用其结果。

表统计信息的散列可以用于快速比较。

这是部分答案,我试图找到一个下界和上界,可能是炸弹的数量。

在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,直到所有单元格都不可拾取。 对最小值列表求和得到下界。