算法
我们得到一个数组input,由称为“行”的大小相等的数组组成。问题中给出的例子如下。
input = [
["X", "X", "O", "X"],
["X", "X", "O", "X"],
["O", "O", "X", "O"],
["O", "O", "X", "O"],
["O", "O", "O", "X"],
["O", "O", "O", "X"]
]
为方便起见,我将元素 input[3][2] #=> "X" 称为 row 3 和 column 2 中的元素(尽管 Ruby 没有二维数组或具有行和列的数组)。
第一步是构造一个数组groups,每个元素(一个“组”)由一个元素组成,其中一行中的一个“X”的索引:
groups
#=> [[[0, 0]], [[0, 1]], [[0, 3]], [[1, 0]], [[1, 1]], [[1, 3]],
# [[2, 2]], [[3, 2]], [[4, 3]], [[5, 3]]]
我们现在考虑groups ([[5, 3]]) 的最后一个元素,并询问它的任何元素(只有一个,[5, 3])是否与任何其他组中的元素在同一个岛中。我们发现它与[[4, 3]] 组中的[4, 3] 位于同一个岛上(因为元素在同一列中,相隔一行)。因此,我们删除最后一个组并将其所有元素(这里只是一个)添加到组[[4, 3]]。我们现在有:
groups
#=> [[[0, 0]], [[0, 1]], [[0, 3]], [[1, 0]], [[1, 1]], [[1, 3]],
# [[2, 2]], [[3, 2]], [[4, 3], [5, 3]]]
我们现在对现在的最后一组 [[4, 3], [5, 3]] 重复该过程。我们必须确定该组中的任何一个元素是否与其他每个组中的任何元素在同一个岛上。它们不是1。因此,我们确定了第一个岛,由位置 [4, 3] 和 [5, 3] 组成。
初始化islands = []后,我们进行如下操作:
islands << groups.pop
现在
groups
#=>#=> [[[0, 0]], [[0, 1]], [[0, 3]], [[1, 0]], [[1, 1]], [[1, 3]],
# [[2, 2]], [[3, 2]]]
islands
#=> [[[4, 3], [5, 3]]]
我们继续这样直到groups 为空,此时islands 的每个元素都是一个岛。
代码
def find_islands(input)
groups = input.each_with_index.with_object([]) { |(row,i),groups|
row.each_with_index { |c,j| groups << [[i,j]] if c == 'X' } }
islands = []
while groups.any?
last_group = groups.pop
idx = groups.each_index.find { |idx| same_island?(groups[idx], last_group) }
if idx.nil?
islands << last_group
else
groups[idx] += last_group
end
end
islands.map(&:sort)
end
def same_island?(group1, group2)
group1.product(group2).any? { |(i1,j1),(i2,j2)|
((i1==i2) && (j1-j2).abs == 1) || ((j1==j2) && (i1-i2).abs == 1) }
end
示例
对于上面给出的数组input,我们得到以下岛屿数组。
find_islands(input)
#=> [[[4, 3], [5, 3]],
# [[2, 2], [3, 2]],
# [[0, 3], [1, 3]],
# [[0, 0], [0, 1], [1, 0], [1, 1]]]
说明
毫无疑问,对于没有经验的 Rubiest 来说,学习理解我所介绍的两种方法的工作原理有很多好处。需要熟悉以下方法:
groups的初始计算
如果这是一个单独的方法(不是一个坏主意),我们将编写以下内容。
def construct_initial_groups(input)
input.each_with_index.with_object([]) { |(row,i),groups|
row.each_with_index { |c,`j| groups << [[i,j]] if c == 'X' } }
end
Ruby 新手可能会觉得这有点不知所措。其实就是把下面的方法收紧的Ruby方式。
def construct_initial_groups(input)
groups = []
i = 0
input.each do |row|
j = 0
row.each do |c|
groups << [[i,j]] if c == 'X'
j += 1
end
i += 1
end
groups
end
从这里到那里的第一步是使用方法Enumerable#each_with_index。
def construct_initial_groups(input)
groups = []
input.each_with_index do |row,i|
row.each_with_index do |c,j|
groups << [[i,j]] if c == 'X'
end
end
groups
end
接下来我们利用Enumerator#with_object的方法得到construct_initial_groups的上述第一种形式。
编写块变量 (|(row,i),groups|) 可能仍然令人困惑。你会学到的
enum = input.each_with_index.with_object([])
#=> #<Enumerator: #<Enumerator: [["X", "X", "O", "X"], ["X", "X", "O", "X"],
# ["O", "O", "X", "O"], ["O", "O", "X", "O"], ["O", "O", "O", "X"],
# ["O", "O", "O", "X"]]:each_with_index>:with_object([])>
是一个枚举器,其元素是通过应用Enumerator#next方法生成的。
(row,i), groups = enum.next
#=> [[["X", "X", "O", "X"], 0], []]
Ruby 使用 disambiguation 或 decomposition 为三个块变量中的每一个赋值:
row
#=> ["X", "X", "O", "X"]
i #=> 0
groups
#=> []
然后我们执行块计算。
row.each_with_index { |c,j| groups << [[i,j]] if c == 'X' }
#=> ["X", "X", "O", "X"].each_with_index { |c,j| groups << [[i,j]] if c == 'X' }
#=> ["X", "X", "O", "X"]
现在
groups
#=> [[[1, 0]], [[1, 1]], [[1, 3]]]
现在生成enum 的第二个元素并将其传递给块并执行块计算。
(row,i), groups = enum.next
#=> [[["O", "O", "X", "O"], 2], [[[1, 0]], [[1, 1]], [[1, 3]]]]
row
#=> ["O", "O", "X", "O"]
i #=> 2
groups
#=> [[[1, 0]], [[1, 1]], [[1, 3]]]
row.each_with_index { |c,j| groups << [[i,j]] if c == 'X' }
#=> ["O", "O", "X", "O"]
现在groups 有两个元素。
groups
#=> [[[1, 0]], [[1, 1]],
# [[1, 3]], [[2, 2]]]
其余的计算类似。
same_island? method
假设
group1 #=> [[0, 0], [1, 0]]
group2 #=> [[0, 1], [1, 1]]
这告诉我们[0, 0]、[1, 0] 在同一个岛上,[0, 1] 和[1, 1] 在同一个岛上,但我们还不知道所有四个坐标是否都在同一个岛上。为了确定是否是这种情况,我们将查看所有坐标对,其中一个来自group1,另一个来自group2。如果我们比较[0, 0] 和[1, 1],我们不能断定它们在同一个岛(或不同的岛)上。但是,当我们比较[0, 0] 和[0, 1] 时,我们看到它们在同一个岛上(因为它们在相邻列中位于同一行),因此我们推断两组的所有元素都在同一个岛上。例如,我们可以将坐标从group2 移动到group1,并从进一步考虑中排除group2。
现在考虑same_island? 方法对这两组执行的步骤。
group1 = [[0, 0], [1, 0]]
group2 = [[0, 1], [1, 1]]
a = group1.product(group2)
#=> [[[0, 0], [0, 1]], [[0, 0], [1, 1]], [[1, 0], [0, 1]],
# [[1, 0], [1, 1]]]
(i1,j1),(i2,j2) = a.first
#=> [[0, 0], [0, 1]]
i1 #=> 0
j1 #=> 0
i2 #=> 0
j2 #=> 1
b = i1==i2 && (j1-j2).abs == 1
#=> true
c = j1==j2 && (i1-i2).abs == 1
#=> false
b || c
#=> true
已发现所考虑的第一对坐标位于同一个岛上。 (其实c的计算不会进行,因为b被发现是true。)
find_islands 已评论
为了展示一切如何组合在一起,我将在方法 find_islands 中添加 cmets。
def find_islands(input)
groups = input.each_with_index.with_object([]) { |(row,i),groups|
row.each_with_index { |c,j| groups << [[i,j]] if c == 'X' } }
islands = []
# the following is the same as: until groups.empty?
while groups.any?
# pop removes and returns last element of `groups`. `groups` is modified
last_group = groups.pop
# iterate over indices of `groups` looking for one whose members
# are on the same island as last_group
idx = groups.each_index.find { |idx| same_island?(groups[idx], last_group) }
if idx.nil?
# last_group is an island, so append it to islands
islands << last_group
else
# groups[idx] members are found to be on the same island as last_group,
# so add last_group to group groups[idx]
groups[idx] += last_group
end
end
# sort coordinates in each island, to improve presentation
islands.map(&:sort)
end
1 也就是说,没有元素groups[i,j] 与[4, 3] 或[5, 3] 位于同一列且相隔一列或位于同一行且相隔一列。
2 模块Kernel 中的实例方法记录在Object 类中。前两段见Kernel,第二段见Object。