蜥蜴法则_如何解决小蜥蜴问题-N皇后问题的有趣转折

蜥蜴法则

by Sachin Malhotra

由Sachin Malhotra

如何解决小蜥蜴问题-N皇后问题的有趣转折 (How to solve the Baby Lizards Problem — a fun twist on the N-Queens problem)

This problem statement was an assignment as a part of my coursework for the Masters program at USC. I had loads of fun while solving it and I decided to share my learnings with the community.

这个问题陈述是我在南加州大学硕士学位课程的一部分作业。 解决问题时,我充满了乐趣,因此决定与社区分享我的经验。

Let’s start with the problem statement.

让我们从问题陈述开始。

问题 (The Problem)

You are a zookeeper in the reptile house. One of your rare lizards has just had several babies. Your job is to find a place to put each baby lizard in a nursery. However, there is a catch: the baby lizards have very long tongues.

您是爬行动物屋里的动物园管理员。 您的一只稀有蜥蜴刚生了几个孩子。 您的工作是找到一个放置每个小蜥蜴的地方。 但是,有一个陷阱:蜥蜴的舌头很长。

A baby lizard can shoot out its tongue and eat any other baby lizard before you have time to save it. As such, you want to make sure that no baby lizard can eat another baby lizard in the nursery (burp).

幼蜥蜴可以吐出舌头,然后在有时间保存之前将其吃掉。 因此,您要确保没有小蜥蜴可以在托儿所里吃另一只小蜥蜴(打bur)。

For each baby lizard, you can place them in one spot on a grid. From there, they can shoot out their tongue up, down, left, right and diagonally as well. Their tongues are very long and can reach to the edge of the nursery from any location.

对于每只小蜥蜴,您可以将它们放在网格上的一个位置。 从那里,他们也可以向上,向下,向左,向右和对角线伸出自己的舌头。 他们的舌头很长,可以从任何位置延伸到苗圃的边缘。

Figure 1 shows in what ways a baby lizard can eat another.

图1显示了小蜥蜴可以以何种方式进食。

In addition to baby lizards, your nursery may have some trees planted in it. Your lizards cannot shoot their tongues through the trees nor can you move a lizard into the same place as a tree.

除小蜥蜴外,您的苗圃中还可以种一些树。 蜥蜴不能在树上射舌,也不能将蜥蜴移到与树相同的地方。

As such, a tree will block any lizard from eating another lizard if it is in the path. Additionally, the tree will block you from moving the lizard to that location.

因此,如果树在路径中,那么它将阻止任何蜥蜴吃掉另一只蜥蜴。 此外,树将阻止您将蜥蜴移动到该位置。

Figure 2 shows some different valid arrangements of lizards:

图2显示了蜥蜴的一些不同的有效排列:

Given an arrangement of trees, we need to output a new arrangement of lizards such that no baby lizard can eat another one. You cannot move any of the trees.

给定一棵树木,我们需要输出一种新的蜥蜴排列,以使小蜥蜴不能再吃另一只。 您不能移动任何树木。

You can find the entire code for this here.

您可以在此处找到完整的代码。

与N皇后相似 (Similarity to N-Queens)

This problem is very similar to the classic N-Queens Problem. Let’s recap some of the constraints in the N-Queens problem.

这个问题与经典的N-皇后问题非常相似。 让我们回顾一下N皇后问题中的一些约束。

  • There can be only one queen per row and column.

    每行和每列只能有一个皇后。
  • There can be only one queen per diagonal and anti-diagonal.

    每个对角线和反对角线只能有一个女王/王后。
  • Considering the above 2 constraints, we cannot place more queens than the number of rows or number of columns.

    考虑到以上两个约束,我们放置的皇后不能超过行数或列数。

Now, we add in a little twist which says that we have trees at certain location in the nursery (read chess board), and the queens (lizards) on either side of a tree cannot attack each other. This changes things big time.

现在,我们稍微加一点扭曲,说我们在苗圃中的某个位置有树(阅读国际象棋棋盘),并且树两边的女王(蜥蜴)不能互相攻击。 这改变了时间。

  • Now, we can have multiple lizards per row, per column.

    现在,我们每行每列可以有多个蜥蜴。
  • Similarly, we can have multiple lizards in a single diagonal or anti-diagonal.

    同样,我们可以在单个对角线或反对角线中有多个蜥蜴。
  • We can place more number of lizards than the number of rows or columns.

    我们可以放置更多数量的蜥蜴,而不是行或列。

Although the problem looks very similar to the standard puzzle of placing N queens on an N*N board, the solution and the complexity turn out to be very different altogether.

尽管问题看上去与在N * N板上放置N个皇后的标准难题非常相似,但解决方案和复杂性却完全不同。

None of the optimized versions of N-Queens fit in directly for this problem because a lot of the optimizations rely on the simple fact that a solution to the N-Queens problems can be represented as a permutation of column subscripts, simply because we have only one lizard per row, column, diagonal and anti-diagonal. We break this assumption, and the optimizations fall apart.

N-Queens的优化版本都不适合直接解决此问题,因为许多优化依赖于一个简单的事实,即N-Queens问题的解决方案可以表示为列下标的置换,这仅仅是因为我们只有每行,每列,对角线和反对角线一只蜥蜴。 我们打破了这个假设,并且优化失败了。

So here in this post, we will discuss a highly optimized backtracking based solution.

因此,在本文中,我们将讨论基于高度优化的回溯的解决方案。

回溯++ (Backtracking++)

The backtracking solution for this problem works in a similar manner to the backtracking solution for the standard N-Queens problem.

此问题的回溯解决方案的工作方式与标准N皇后问题的回溯解决方案相似。

The solution for this problem is based on the following idea.

该问题的解决方案基于以下思想。

Given a cell [i, j], we can either place a lizard, or not place a lizard. Any one of our choices can lead to a solution. So we try both.

给定一个单元格[i, j] ,我们可以放置蜥蜴,也可以不放置蜥蜴。 我们的任何选择都可以找到解决方案。 所以我们都尝试。

The biggest invariant in this algorithm is that we always move from left to right across the board.

该算法最大的不变性是我们总是从左到右全盘移动。

Suppose there is a tree at location [3,4]. Its masking effect (if any) would only be visible once we cross the cell [3,4] in our recursion and move forward. Not before that.

假设在位置[3,4]处有一棵树。 仅当我们在递归中越过单元格[3,4]并向前移动时,它的掩盖效果(如果有)才可见。 在那之前。

Before we get to the actual pseudocode for the problem, there are some other components of the algorithm that I would like to explain. This would make the understanding of the pseudocode much simpler.

在解决该问题的实际伪代码之前,我想解释一下该算法的其他一些组件。 这将使对伪代码的理解更加简单。

安全检查和O(1)难题 (The Safety Check and the O(1) conundrum)

If you’ve taken a look at my previous article that discusses different algorithmic solutions to the N-Queens puzzle, you might understand what the issue really is.

如果您看了我以前的文章 ,该文章讨论了N-Queens难题的不同算法解决方案,那么您可能会理解问题的实质。

We get almost 5X speed improvement on a 14 * 14 chessboard where we have to place 14 queens, after converting the safety check function to O(1) from O(N). So it was worth spending time to figure out an algorithm that would tell us in constant time if it is safe to place a queen on a given cell [i, j].

在将安全检查功能从O(N)转换为O(1)之后,我们在14 * 14棋盘上必须放置14个皇后的速度几乎提高了5倍。 因此,值得花时间找出一种算法,该算法可以在恒定时间内告诉我们将皇后放在给定单元格[i,j]上是否安全。

For reference, let’s look at how we did it back in the normal N-Queens.

作为参考,让我们看看我们是如何在普通N皇后乐队中做到这一点的。

We made use of some additional data structures to tell us if a queen had been placed in a certain diagonal, anti-diagonal, row or column in O(1) time and using these we could tell if it was safe to place a queen on a given cell [i, j].

我们利用一些附加的数据结构来告诉我们是否在O(1)时间内将皇后放置在特定的对角线,反对角线,行或列中,并使用这些数据可以判断将皇后放置在上面是否安全给定的单元格[i,j]。

However, if you’ve read through the problem statement carefully, we can now have trees in some locations on the board and if there is a tree between the current cell and an attacker lizard (it can be on a row, column, or any of the two diagonals), then it is in fact safe to place a lizard on the current cell. This is because the tree masks the attack, making the cell safe for a new lizard.

但是,如果您已经仔细阅读了问题说明,那么我们现在可以在板上的某些位置放置树,并且如果当前单元格和攻击者蜥蜴之间有树(可以位于行,列或其他任何位置)两个对角线中的一个),那么实际上可以安全地将蜥蜴放在当前单元格上。 这是因为树掩盖了攻击,使牢房对于新蜥蜴而言是安全的。

This changes things, a lot ?.

这会改变很多事情。

Let’s start with what data structures we need for the implementation.

让我们从实现所需的数据结构开始。

使用的数据结构 (The Data Structures Used)

Let’s go over them one by one.

让我们一一介绍。

  • tree_locations — this is just a dictionary that tells us if a given cell [i, j] contains a tree. This is populated right at the start of our solver.

    tree_locations —这只是一个字典,它告诉我们给定的单元格[i,j]是否包含树。 这是在我们的求解器开始时填充的。

  • The four data structures rows, columns, diagonals and anti-diagonals are used to simply tell us if there is a lizard in the respective r, c, r — c, r + c respectively. For this problem however, they represent integer values rather than boolean.

    四个数据结构的行,列,对角线和反对角线用于简单地告诉我们在各自的r, c, r — c, r + c是否存在蜥蜴。 但是,对于此问题,它们表示整数值,而不是布尔值。

    These four data structures store either 1 or -1 depending upon if we are placing a lizard at a current cell [i, j] or we are encountering a tree at a given cell [i, j].

    这四个数据结构存储1或-1,具体取决于我们是将蜥蜴放在当前单元格[i,j]还是遇到给定单元格[i,j]上的树。

    So the recursion proceeds from one cell to another and can either encounter a tree at a given cell [i, j] or it can encounter an empty cell in which case we have to call the

    因此,递归会从一个单元格进行到另一单元格,并且可能遇到给定单元格[i,j]上的树,或者可能遇到空单元格,在这种情况下,我们必须将

    is_cell_safe function to verify if we can place a lizard.

    is_cell_safe函数来验证是否可以放置蜥蜴。

    I will come to how the values are updated in these four data structures namely

    我将介绍如何在这四个数据结构中更新值

    rows, columns, diagonals and anti-diagonals later on.

    rows columns diagonals anti-diagonals 稍后的。

  • is_there_queen_in_this_column this is a dictionary that simply stores the number of lizards that we placed in a given column. This is used as a part of a pruning heuristic employed to reduce the size of the search space.

    is_there_queen_in_this_column 这是一个仅存储我们在给定列中放置的蜥蜴数量的字典。 这被用作修剪试探法的一部分,以减少搜索空间的大小。

  • next_position_same_column — this tells us for every [i, j] what is the next spot in the same column where we could try and place a new lizard. In the normal N-Queens problem, we can only place a single queen in a column, but in this case we can have multiple queens (lizards).

    next_position_same_column —这告诉我们每个[i,j]在同一列中的下一个位置是什么,我们可以尝试放置新的蜥蜴。 在正常的N皇后问题中,我们只能在列中放置一个皇后,但在这种情况下,我们可以有多个皇后(蜥蜴)。

    So, after placing a lizard at cell [i, j], we need the location of the first tree in the same column and say that is [k, j]. The next available location for placing a lizard in that column would then be [k+1, j]. This array is used as a part of this optimization.

    因此,在将蜥蜴放置在单元格[i,j]之后,我们需要在同一列中放置第一棵树的位置,并说这是[k,j]。 在该列中放置蜥蜴的下一个可用位置将是[k + 1,j]。 此数组用作此优化的一部分。

  • Finally, is_there_a_tree_ahead is a dictionary which tells us if there is a tree somewhere in the board after this column (including this column as well). This is also populated once as a part of the initial preprocessing. This is also used as a part of the pruning heuristic referred to above while describing is_there_queen_in_this_column.

    最后, is_there_a_tree_ahead 是一本字典,它告诉我们此列之后(包括此列)在董事会的某处是否有树。 作为初始预处理的一部分,它也会被填充一次。 在描述is_there_queen_in_this_column这也用作上面提到的修剪启发式方法的一部分。

预处理功能 (The Preprocess function)

The preprocess function is called initially before our algorithm starts execution and all it does is fills up some of the data structures discussed above.

在我们的算法开始执行之前,会先调用预处理函数,然后它会填充上面讨论的某些数据结构。

  • The trees_populator function is pretty straightforward. It fills up the dictionaries tree_locations and is_there_a_tree_ahead.

    trees_populator 功能非常简单。 它填充了字典tree_locations is_there_a_tree_ahead

  • The function find_next_largest considers each column as consisting of 0s and 2s where a 0 represents an empty cell and a 2 represents a tree. For every cell, it finds out the next largest element or in other words, the nearest tree to that location in that column. We call the find_next_largest function for every column on the board.

    函数find_next_largest 认为每一列都由0和2组成,其中0代表一个空单元格,而2代表一棵树。 对于每个单元格,它会找到下一个最大的元素,或者换句话说,就是该列中与该位置最近的树。 我们称之为find_next_largest 功能 板上的每一列。

For a better understanding of this algorithm, refer to this overview.

为了更好地理解该算法,请参阅本概述

is_cell_safe函数 (is_cell_safe Function)

A positive value in any of the dictionaries row, column, diagonal, anti-diagonal means there is a lizard that is would potentially attack another lizard that we’re trying to place at [row, column].

在任何rowcolumndiagonalanti-diagonal的字典中为正值 意味着有一只蜥蜴有可​​能攻击我们试图放置在[行,列]的另一只蜥蜴。

This function looks very similar to the one we used for the normal N-Queens. The important part is how we update the values in these data structures.

此功能看起来与我们用于普通N皇后的功能非常相似。 重要的是如何更新这些数据结构中的值。

标记已访问,未标记已访问和哈希实用程序 (Mark Visited, Unmark Visited and Hash Util)

The function hash_util is a common function used to update the values for all the four data structures (namely rows, columns, diagonals and anti-diagonals).

函数hash_util是一个通用函数,用于更新所有四个数据结构(即rowscolumnsdiagonalsanti-diagonals )的值。

This function is called both, when we are marking a lizard or a tree, or, when we are unmarking either of them. The marking and unmarking are simply processing before a recursive call and undoing whatever we processed, after the recursive call is over.

当我们标记蜥蜴或树时,或者当我们未标记它们中的任何一个时,此函数都称为。 标记和取消标记只是在递归调用之前处理,而在递归调用结束之后撤消我们处理的所有内容。

Remember the invariant discussed in this problem: we move from left to right across the board. Once we have encountered a tree at a certain location (i, j) during the recursion, it would be protecting lizards from each other for all the cells [i+1, j] and all columns k > j.

记住在这个问题中讨论的不变式:我们从左到右全盘移动。 一旦在递归过程中的某个位置(i,j)遇到了树,它将保护所有单元格[i + 1,j]和所有列k> j彼此之间的蜥蜴。

The result variable is very important here. For example, we encountered a tree at say [3,0] and there was a lizard at [1,0]. Now moving onwards, this tree is masking the effect of the lizard at [1,0] — at least for this column — and we need to bring this effect into consideration somewhere.

result变量在这里非常重要。 例如,我们在[3,0]遇到一棵树,在[1,0]上遇到了一只蜥蜴。 现在继续前进,这棵树正在掩盖蜥蜴在[1,0]的效果-至少对于本专栏而言-我们需要在某个地方考虑​​这种效果。

So, in this case:

因此,在这种情况下:

  • is_marking = True,

    is_marking = True

  • is_tree = True,

    is_tree = True

  • dictionary = column,

    dictionary = column

  • dictionary[col] > 0 (We store 1 whenever we place a lizard in that column). This is because we have already placed a lizard in the column 0 (at 1,0) and there was no tree discovered earlier that would hide the lizard’s effect for cell [3,0] in our example.

    dictionary[col] > 0(只要将蜥蜴放在该列中,我们就会存储1)。 这是因为我们已经在第0列(位于1,0)中放置了一只蜥蜴,并且在我们的示例中没有发现任何树可以隐藏蜥蜴对单元格[3,0]的影响。

  • value_to_add = -1 (For a tree it’s -1, for a lizard it’s 1)

    value_to_add = -1(对于树,则为-1,对于蜥蜴,则为1)

So now, dictionary[col] = -1 and we return 1 as the result meaning that encountering a tree in the given (row, column) did in fact have some masking effect. We need to record this masking effect because this would be used at the time of undoing after recursion.

因此,现在, dictionary[col] = -1,我们返回1作为结果,这意味着在给定的(行,列)中遇到树实际上确实具有一定的屏蔽效果。 我们需要记录此掩盖效果,因为在递归后撤消时会使用此掩盖效果。

Now consider two other functions that form the main component of the algorithm.

现在考虑构成算法主要组成部分的其他两个函数。

马克访问 (Mark Visited)

We call this function in two cases. One is when we encounter a tree, and one is when we want to place a lizard. So, accordingly we have used a boolean variable to tell us why this function has been called.

我们在两种情况下调用此函数。 一种是当我们遇到一棵树时,另一种是当我们想要放置蜥蜴时。 因此,因此,我们使用了一个布尔变量来告诉我们为什么调用此函数。

In the case of a tree, we set the value to -1, otherwise it’s +1. Then, we update the four data structures. The logic is the same for all four of them. It’s just the key that changes for each one.

对于树,我们将值设置为-1,否则为+1。 然后,我们更新四个数据结构。 这四个逻辑都是相同的。 只是每一个改变的关键。

Remember, row — col is used to uniquely identify a diagonal and row + colis used to uniquely identify an anti-diagonal.

请记住, row — col用于唯一标识对角线,而row + col用于唯一标识反对角线。

Also note that we store the quadruple of return values for the four data structures in did_tree_affect dictionary. This let’s us know if encountering a tree at the location (row, col) had any effect at all i.e. masking. This data is used during the undo operation.

还要注意,我们将四个数据结构的返回值的四倍存储在did_tree_affect 字典。 这让我们知道在位置(行,列)遇到树是否有任何效果,即遮罩。 此数据在撤消操作期间使用。

取消标记访问 (Unmark Visited)

We know that a positive value in any of the four dictionaries means that the given cell is not safe to place a lizard.

我们知道四个字典中任何一个的正值都意味着给定的单元格放置蜥蜴是不安全的。

The undo operation is pretty simple for a lizard. If we are calling unmark_visited function for a lizard, it means the cell was safe enough before we placed a lizard there, so we just put a value of -1 in all the four dictionaries. (Remember, a positive value in either of rows, columns, diagonals or antidiagonals would break the is_cell_safe function for that cell)

对于蜥蜴来说 ,撤消操作非常简单。 如果我们要为蜥蜴调用unmark_visited函数,则意味着在将蜥蜴放到那里之前,该单元格已经足够安全了,因此我们在所有四个字典中都输入了-1值。 (请记住,行,列,对角线或对角线中的任何一个正值都会破坏该单元格的is_cell_safe函数)

In case the function unmark_visited was called for a tree, we retrieve values from did_tree_affect for the given [row, col] and use these values to revert the dictionaries. The sense in this is that suppose that we encountered a tree at given [row, col] and it masked the lizard’s effect for the diagonal and the column moving forward. See the following figure:

如果为一棵树调用了unmark_visited函数,我们将从did_tree_affect检索给定[row,col]的值,并使用这些值还原字典。 这样做的意义是假设我们在给定的[row,col]处遇到一棵树,并且它掩盖了蜥蜴对角线和圆柱前进的影响。 参见下图:

When we have to revert seeing the tree in the recursion, we basically have to revert it’s masking effect. That is what the did_tree_affect dictionary is used for.

当我们不得不在递归中看到树时,我们基本上必须恢复它的掩盖效果。 这就是did_tree_affect词典的用途。

Now that we have all our dictionaries in place, we can finally look at the actual DFS function that does all our heavy lifting for finding a solution.

现在我们已经有了所有字典,我们终于可以看一下实际的DFS功能,该功能为找到解决方案做了所有的繁重工作。

回溯求解器 (Backtracking Solver)

The code is seemingly complicated and the post would get extremely long if I started explaining it in detail. I might be able to clarify the doubts in the comments section. For now, I’ll write a detailed version of the pseudocode for completion.

代码似乎很复杂,如果我开始详细解释它,那么这篇文章会变得很冗长。 我也许能够在评论部分中澄清疑问。 现在,我将编写详细的伪代码版本以供完成。

1. Start at the cell (0, 0)
2. For a given cell (i, j)     a. If all the lizards have been placed, print the solution and return True.
b. Check if the current cell has a tree.          b1. Call mark_visited function to update the 4 dictionaries with possible masking effects due to this tree.
c. If the current cell isn't a tree and a lizard can be placed         c1. Call mark visited for [i, j] as a lizard.         c2. Add [i, j] to the solution set.          c3. Increment column j as containing one more lizard.         c4. Find the next row number to recurse on in the column j. If there is such a row number say r, then recurse on [r, j]. Else recurse on [0, j+1]         c5. Unmark the current cell. Call function unmark_visited for [i, j]         c6. Decrement column j as it contains one less lizard now.
d. We may want to have a branch in our recursive solution where we did not place a lizard at [i, j] and simply moved forward. OR, we couldn't place a lizard at [i, j] and we now have to move forward.          d1. if [i + 1] < n, recurse on [i+1, j]         d2. else [PRUNING HEURISTIC]              d2.1 check if                    * we did not place any lizard in the current col.                   * there is no tree in the current col and ahead.                    * number of lizards left to be placed are more than the number of columns left.                    * If yes to all 3, then BACKTRACK.              d2.2 Else, recurse on [0, j+1]
e. If the current cell was in-fact a tree, then call unmark_visited to undo its effects.

That is the most apt pseudocode that I could come up with for the DFS based solver. This is exactly how the function dfs is structured.

对于基于DFS的求解器,这是我最能想到的伪代码。 这正是dfs函数的方式 结构化。

With this logic, the largest test-case that I was able to solve was to place 97,000 lizards on a 1000 * 1000 board. It took around 2 seconds to run.

按照这种逻辑,我能够解决的最大测试案例是将97,000只蜥蜴放在1000 * 1000的板上。 运行大约需要2秒钟。

Now I’m telling you, that this might sound like a huge feat, but it isn’t actually. This was pretty easy for the algorithm. Question for you guys is to figure out the why behind this ?. Let me know in the comment section !

现在,我告诉您,这听起来像是一项巨大的壮举,但实际上并非如此。 对于算法来说,这非常容易。 你们的问题是弄清楚这背后的原因吗? 让我知道在评论部分!

Also, if you do come up with some other simpler approach to solve the problem, I would love to discuss that as well. Let me know in the comment section itself.

另外,如果您确实提出了其他一些更简单的方法来解决问题,我也很乐意对此进行讨论。 在评论部分中让我知道。

Hope you liked the article and enjoyed as much I did while solving this problem. If you liked this post, do spread the love (❤) as much as possible. Cheers!

希望您喜欢这篇文章,并喜欢我在解决此问题时所做的一切。 如果您喜欢这篇文章,请尽量传播爱(❤)。 干杯!

翻译自: https://www.freecodecamp.org/news/how-to-solve-the-baby-lizards-problem-a-fun-variant-on-the-n-queens-problem-a6980f5e72a/

蜥蜴法则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值