Bomb dropping algorithm

后端 未结 30 826
挽巷
挽巷 2021-01-29 16:55

I have an n x m matrix consisting of non-negative integers. For example:

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


        
相关标签:
30条回答
  • 2021-01-29 17:31

    This would be a greedy approach:

    1. Calculate a "score" matrix of order n X m, where score[i][j] is the total deduction of points in the matrix if position (i,j) is bombed. (Max score of a point is 9 and min score is 0)

    2. Moving row wise, find and pick the first position with highest score (say (i,j)).

    3. Bomb (i,j). Increase bomb count.

    4. If all elements of the original matrix are not zero, then goto 1.

    I have my doubts that this is the optimal solution though.

    Edit:

    The Greedy approach I posted above, while it works, most probably doesn't give us the optimal solution. So I figured should add some elements of DP to it.

    I think we can agree that at any point of time, one of the positions with the highest "score" (score[i][j] = total deduction of points if (i,j) is bombed) must be targeted. Starting with this assumption, here's the new approach:

    NumOfBombs(M): (returns the minimum number of bombings required)

    1. Given a Matrix M of order n X m. If all elements of M are zero, then return 0.

    2. Calculate the "score" matrix M.

      Let the k distinct positions P1,P2,...Pk (1 <= k <= n*m), be the positions in M with the highest scores.

    3. return (1 + min( NumOfBombs(M1), NumOfBombs(M2), ..., NumOfBombs(Mk) ) )

      where M1,M2,...,Mk are the resulting matrices if we bomb positions P1, P2, ..., Pk respectively.

    Also, if we want the order of positions to nuke in addition to this, we would have to keep track of the results of "min".

    0 讨论(0)
  • 2021-01-29 17:33

    I had to stop at only a partial solution since I was out of time, but hopefully even this partial solution provides some insights on one potential approach to solving this problem.

    When faced with a hard problem, I like to come up with simpler problems to develop an intuition about the problem space. Here, the first step I took was to reduce this 2-D problem into a 1-D problem. Consider a line:

    0 4 2 1 3 0 1
    

    Somehow or another, you know you will need to bomb at or around the 4 spot 4 times to get it down to 0. Since left of the spot is a lower number, there is no benefit to bombing the 0 or the 4 over bombing the 2. In fact, I believe (but lack a rigorous proof) that bombing the 2 until the 4 spot goes down to 0 is at least as good as any other strategy to get that 4 down to 0. One can proceed down the line left to right in a strategy like this:

    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
    

    A couple sample bombing orders:

    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 idea of starting with a number that needs to go down some way or another is an appealing one because it suddenly becomes attainable to find a solution that as some claim to being at least as good as all other solutions.

    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.

    Given the observation about corner pieces, we can say for sure that we know the optimal strategy to go from any starting board to a board with zeros on all corners. This is an example of such a board (I borrowed the numbers from the two linear boards above). I've labelled some spaces differently, and I'll explain why.

    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
    

    One will notice at the top row really closely resembles the linear example we saw earlier. Recalling our earlier observation that the optimal way to get the top row all down to 0 is to bomb the second row (the x row). There is no way to clear the top row by bombing any of the y rows and no additional benefit to bombing the top row over bombing the corresponding space on the x row.

    We could apply the linear strategy from above (bombing the corresponding spaces on the x row), concerning ourselves only with the top row and nothing else. It would go something like this:

    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.

    At this point, I can take a step back down in complexity and focus just one one corner. Let's consider this one:

    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 . . . .
    

    For this one, it is clear to me that the optimal solution is to bomb y 5 times and z 5 times. Let's go one step further.

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

    Here, it feels similarly intuitive that the optimal solution is to bomb a and b 6 times and then x 4 times.

    Now it becomes a game of how to turn those intuitions into principles we can build on.

    Hopefully to be continued!

    0 讨论(0)
  • 2021-01-29 17:35

    This does a breadth-search for the shortest path (a series of bombings) through this "maze" of positions. No, I cannot prove that there is no faster algorithm, sorry.

    #!/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))
    
    0 讨论(0)
  • 2021-01-29 17:36

    Pólya says "If you can't solve a problem, then there is an easier problem you can solve: find it."

    The obvious simpler problem is the 1-dimensional problem (when the grid is a single row). Let's start with the simplest algorithm - greedily bombing the biggest target. When does this go wrong?

    Given 1 1 1, the greedy algorithm is indifferent to which cell it bombs first. Of course, the centre cell is better - it zeros all three cells at once. This suggests a new algorithm A, "bomb to minimise the sum remaining". When does this algorithm go wrong?

    Given 1 1 2 1 1, algorithm A is indifferent between bombing the 2nd, 3rd or 4th cells. But bombing the 2nd cell to leave 0 0 1 1 1 is better than bombing the 3rd cell to leave 1 0 1 0 1. How to fix that? The problem with bombing the 3rd cell is that it leaves us work to the left and work to the right which must be done separately.

    How about "bomb to minimise the sum remaining, but maximise the minimum to the left (of where we bombed) plus the minimum to the right". Call this algorithm B. When does this algorithm go wrong?


    Edit: After reading the comments, I agree a much more interesting problem would be the one dimensional problem changed so that the ends join up. Would love to see any progress on that.

    0 讨论(0)
  • 2021-01-29 17:36

    There is no need to transform the problem to linear sub-problems.

    Instead use a simple greedy heuristic, which is to bomb the corners, starting with the largest one.

    In the given example there are four corners, { 2, 1, 6, 4 }. For each corner there is no better move than to bomb the cell diagonal to the corner, so we know for a fact our first 2+1+6+4 = 13 bombings must be in these diagonal cells. After doing the bombing we are left with a new matrix:

    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 
    

    After the first 13 bombings we use the heuristic to eliminate 3 0 2 via three bombings. Now, we have 2 new corners, { 2, 1 } in the 4th row. We bomb those, another 3 bombings. We have reduced the matrix to 4 x 4 now. There is one corner, the upper left. We bomb that. Now we have 2 corners left, { 5, 3 }. Since 5 is the largest corner we bomb that first, 5 bombings, then finally bomb the 3 in the other corner. The total is 13+3+3+1+5+3 = 28.

    0 讨论(0)
  • 2021-01-29 17:36

    Brute Force !

    I know it is not efficient, but even if you find a faster algorithm, you can always test against this result to know how accurate it is.

    Use some recursion, like this:

    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);
    
    }
    

    You can make this more efficient by caching, if different way lead to same result, you shouldn't repeat the same steps.

    To elaborate:

    if bombing cell 1,3,5 leads to the same result as bombing cell 5,3,1 , then, you shouldn't re-do all the next steps again for both cases, only 1 is enough, you should store somewhere all table states and use its results.

    A hash of table stats can be used to do fast comparison.

    0 讨论(0)
提交回复
热议问题