4

我一直在研究一些 Project Euler 问题,而且在大多数情况下,我都做得很好。问题 18,虽然真的难倒我。

从树的顶部开始,我应该找到通向最大和的路径

      3
    7   4
  2   4   6
8   5   9   3

在这种情况下,有 24 条可能的路径,即 4!最好的可能路径是3 -> 7 -> 4 -> 9,总和为 23。

我试图通过复制示例来解决问题。

array = [[3],[7,4],[2,4,6],[8,5,9,3]]
array.each_slice(1){|s|p s}                          => This prints the tree

我得到的答案在极少数情况下是正确的,但这并不是真正合法的。

sum = []
array.each{|a|sum.push(a.sample)}
return sum

这种方法基本上只是选择一条随机路径,即使有这个简单的例子,仍然只有 24 分之一的机会让它正确。

我试过像

level_two = []
level_three = []
for i in 0..array.length-1
  for j in 0..array[1].length-1
    level_two.push([array[0], array[1][i])        => Now level 2 has 2 paths - [3,7] & [3,4]
    for k in 0..array[2].length-1
      level_three.push([level_two[0],array[2][k], [level_two[1], array[2][k])
    end
  end
end

但此时我什至无法跟踪我正在使用哪些变量。

我试过each_cons、combination 和zip,都没有奏效。

有谁知道解决这个问题的策略?

编辑:这不会给出问题的答案,而只是给出示例的答案。然后,我将在主要问题上应用该设计模式。

4

4 回答 4

2

With these types of problems, my first step is to understand the relationships between the array indices. Given an array at level n of this tree, what child elements at array level n + 1 can a parent element at index i (in array level n) access? The answer is elements at i and i + 1.

For example: Let [2, 4, 6] be the array at level 2 and [8, 5, 9, 3] be the array at level 3. What child elements can element 6 (at index 2) access? 9, 3 can be accessed because their indices are 2, 3.

Now we can create a recursive solution to this problem:

@res = [] # All paths will be stored here
def paths(arr, temp, lvl = 0, idx = 0)
  if arr[lvl] # we haven't hit end of path
    # loop through elements at i and i + 1
    arr[lvl][idx..idx+1].each_with_index do |x, i|
      # go one level deeper into tree
      paths(arr, temp.dup << x, lvl + 1, i)
    end
  else
    @res << temp # `temp` has complete path, push it into results
  end
end

paths(array, []) # initialize temporary path to empty array

@res.map { |x| x.reduce(:+) }.max # => 23
于 2014-11-12T18:31:24.810 回答
2

这个问题是递归的完美用例。对于术语,假设它T[3][0]是第 3 行中的第 0 个元素,从 0 开始计数(在您的示例中为 8)。

从树的顶部开始,通往最大和的路径是什么?首先,让我们稍微改写一下:从 开始的最大和的路径是什么T[0][0]?好吧,那将T[0][0]加上从T[1][0]or开始的路径T[1][1](以较大的路径为准)。找到路径本身对于找到最大和是偶然的,所以我们只关注它。

假设这S[x][y]是从 开始的路径的最大总和T[x][y]。然后从前面的观察我们得到

S[x][y] = T[x][y] + MAX(S[x + 1][y], S[x + 1][y + 1])

除非x是树的最后一行。让R我们的树的最后一行,在这种情况下

S[R][y] = T[R][y]

在 Ruby 中,我们可以这样编写函数:

def largest_sum_path(tree, x = 0, y = 0)
  path = [tree[x][y]]

  return path if x == tree.length - 1

  left  = largest_sum_path(tree, x + 1, y)
  right = largest_sum_path(tree, x + 1, y + 1)

  return path.concat([left, right].max_by { |p| p.reduce(:+) })
end

但是,我怀疑这种方法对于大树来说会非常慢。我会留给你看如何优化它。

于 2014-11-12T18:06:38.333 回答
1

从最后一行开始。将其替换为由每个连续对的最大值组成的行,因此8 5 9 3-> 8 9 9。将这些值添加到它上面的行(2 4 6 -> 10 13 15)。重复直到第一行。(10 13 15 -> 13 15, 7 4-> 20 19 -> 20; 3 -> 23.

于 2014-11-12T19:07:33.563 回答
1

我就是这样做的。

代码

def longest_path(arr)
  return nil if arr.empty?

  h = { len: arr.first.first, path: [] }
  return h if arr.size == 1

  arr[1..-1].reduce([h]) do |l,row|
    h = l.first
    left = { len: h[:len]+row.first, path: h[:path]+[:l] }
    mid = l.each_cons(2).to_a.zip(row[1..-2]).map do |(h1,h2),v|
            if h1[:len] >= h2[:len]
              { len: h1[:len]+v, path: h1[:path]+[:r] }
            else
              { len: h2[:len]+v, path: h2[:path]+[:l] }
            end
          end
    h = l.last
    right = { len: h[:len]+row.last, path: h[:path]+[:r] }
    [left, *mid, right]
  end.max_by { |h| h[:len] }
end

例子

a = [   [3],
       [7,4],
      [2,4,6],
     [8,5,9,3]]

longest_path a
  #=> {:len=>23, :path=>[:l, :r, :r]}

因此,最长路径的长度为 23。从3第一行向下和向左 ( :l) 到7第二行,向下和向右 ( :r) 到4第三行,向下和向右到9最后一行:3+7+4+9 => 23

解释

这个问题与算法的选择同样重要。在我看来,后者相当明显:求解一排,用它求解两排,依此类推。

考虑上面的示例数组a

arr = a
arr.empty? #=> false, so continue

h = { len: arr.first.first, path: [] }
  #=> {:len=>3, :path=>[]}
return h if arr.size == 1 # arr.size => 4, so continue

作为

arr[1..-1] => [[7, 4], [2, 4, 6], [8, 5, 9, 3]]

reduce传递[h][7, 4]进入块并分配块变量:

l   = [{ len: arr.first.first, path: [] }]
row = [7, 4]

然后计算:

h     = l.first
  #=> {:len=>3, :path=>[]}
left  = { len: h[:len]+row.first, path: h[:path]+[:l] }
  #=> {:len=>10, :path=>[:l]}
mid   = []
h     = l.last
  #=> {:len=>3, :path=>[]}
right = { len: h[:len]+row.last, path: h[:path]+[:r] }
  #=> {:len=>7, :path=>[:r]}
[left, *mid, right]
  #=> [{:len=>10, :path=>[:l]}, {:len=>7, :path=>[:r]}]

mid => []因为each_cons(2)在大小为 1 的数组上执行。

上面的最后一行提供了到第二行中两个元素中每一个元素的最长路径的信息。对于第一个元素 ( 7),路径是有长度10的,从第一行中的唯一元素 ( 3) 开始,然后向下和“左” ( :l) 到给定元素。

正如在块[left, *mid, right]的最后一行中计算的那样reduce,块变量l被赋予该值以处理下一行arr

l   = [{:len=>10, :path=>[:l]}, {:len=>7, :path=>[:r]}]
row = [2, 4, 6]

接下来我们计算以下内容:

left = { len: h[:len]+row.first, path: h[:path]+[:l] }
  #=> {:len=>5, :path=>[:l]}
l.each_cons(2).to_a.zip(row[1..-2]).map do |(h1,h2),v|
   if h1[:len] >= h2[:len]
     { len: h1[:len]+v, path: h1[:path]+[:r] }
   else
     { len: h2[:len]+v, path: h2[:path]+[:l] }
   end
end
  #=> [{:len=>14, :path=>[:l, :r]}]
h = l.last
  #=> {:len=>7, :path=>[:r]}
right = { len: h[:len]+row.last, path: h[:path]+[:r] }
  #=> {:len=>13, :path=>[:r, :r]}
[left, *mid, right]
  #=> [{:len=>5, :path=>[:l]}, {:len=>14, :path=>[:l, :r]},
  #    {:len=>13, :path=>[:r, :r]}]

left和的计算right类似于对 的前一个元素所做的计算arr。我们来看看计算mid

pairs = l.each_cons(2).to_a
  #=>   [[{:len=>10, :path=>[:l]}, {:len=>7, :path=>[:r]}]]
vals  = pairs.zip(row[1..-2])
  #=>   pairs.zip([4])
  #=>   [[[{:len=>10, :path=>[:l]}, {:len=>7, :path=>[:r]}], 4]]

vals是一个包含一个元素的数组。该元素被传递到map,分解并分配给块变量:

h1       = {:len=>10, :path=>[:l]}
h2       = {:len=> 7, :path=>[:r]}
v        = 4
h1[:len] #=> 10
h2[:len] #=> 7

作为10 > 7,我们执行:

{ len: h1[:len]+v, path: h1[:path]+[:r] }

这是 的值midl现在为块值reduce分配了以下结果[left, *mid, right]

l = [{:len=> 5, :path=>[:l]}, {:len=>14, :path=>[:l, :r]},
     {:len=>13, :path=>[:r, :r]}]

并开始处理第三行。reduce返回:

d = [{:len=>20, :path=>[:l, :l, :l]}, {:len=>19, :path=>[:l, :r, :l]},
     {:len=>23, :path=>[:l, :r, :r]}, {:len=>16, :path=>[:r, :r, :r]}]

它提供了描述最后一行每个元素的最长路径的信息。最后一步是:

d.max_by { |h| h[:len] }
  #=> {:len=>23, :path=>[:l, :r, :r]}
于 2014-11-13T00:19:54.293 回答