【问题标题】:How does `for` work in this recursive Clojure code?`for` 在这个递归 Clojure 代码中是如何工作的?
【发布时间】:2014-03-07 07:56:09
【问题描述】:

Clojure 初学者在这里。这是我试图理解的一些代码,来自http://iloveponies.github.io/120-hour-epic-sax-marathon/sudoku.html(一页相当不错的 Clojure 入门课程):


Subset sum is a classic problem. Here’s how it goes. You are given:

    a set of numbers, like #{1 2 10 5 7}
    and a number, say 23

and you want to know if there is some subset of the original set that sums up to the target. 
We’re going to solve this by brute force using a backtracking search.

Here’s one way to implement it:

(defn sum [a-seq]
  (reduce + a-seq))

(defn subset-sum-helper [a-set current-set target]
  (if (= (sum current-set) target)
    [current-set]
    (let [remaining (clojure.set/difference a-set current-set)]
      (for [elem remaining
            solution (subset-sum-helper a-set
                                        (conj current-set elem)
                                        target)]
        solution))))

(defn subset-sum [a-set target]
  (subset-sum-helper a-set #{} target))

So the main thing happens inside subset-sum-helper. First of all, always check if we have found 
a valid solution. Here it’s checked with

  (if (= (sum current-set) target)
    [current-set]

If we have found a valid solution, return it in a vector (We’ll see soon why in a vector). Okay, 
so if we’re not done yet, what are our options? Well, we need to try adding some element of 
a-set into current-set and try again. What are the possible elements for this? They are those 
that are not yet in current-set. Those are bound to the name remaining here:

    (let [remaining (clojure.set/difference a-set current-set)]

What’s left is to actually try calling subset-sum-helper with each new set obtainable 
in this way:

      (for [elem remaining
            solution (subset-sum-helper a-set
                                        (conj current-set elem)
                                        target)]
        solution))))

Here first elem gets bound to the elements of remaining one at a time. For each elem, 
solution gets bound to each element of the recursive call

            solution (subset-sum-helper a-set
                                        (conj current-set elem)
                                        target)]

And this is the reason we returned a vector in the base case, so that we can use for 
in this way.

果然,(subset-sum #{1 2 3 4} 4) 返回(#{1 3} #{1 3} #{4})

但是为什么subset-sum-helper的第3行必须返回[current-set]?这不会返回([#{1 3}] [#{1 3}] [#{4}]) 的最终答案吗?

我尝试删除第 3 行中的括号,使函数开始如下:

(defn subset-sum-helper [a-set current-set target]
  (if (= (sum current-set) target)
    current-set
    (let ...

现在(subset-sum #{1 2 3 4} 4) 返回(1 3 1 3 4),这使得let 看起来不是累积三个集合#{1 3}、#{1 3} 和#{4},而只是“裸”数字,给(1 3 1 3 4)

所以subset-sum-helper 在递归计算中使用列表理解for,我不明白发生了什么。当我尝试可视化这个递归计算时,我发现自己在问,“那么当

(subset-sum-helper a-set
   (conj current-set elem)
   target)

不返回答案,因为在给定起点的情况下没有答案?”(我最好的猜测是它返回 [] 或类似的东西。)我不明白教程作者写的时候是什么意思, “这就是我们在基本情况下返回向量的原因,以便我们可以通过这种方式使用for。”

如果您能给我任何帮助,我将不胜感激。谢谢!

【问题讨论】:

  • 我现在已经更新了我的答案

标签: for-loop recursion clojure list-comprehension


【解决方案1】:

subset-sum-helper 函数始终返回解决方案的序列。当不满足target 时,for 表达式末尾的solution 主体会枚举这样一个序列。当遇到target 时,只有一种解决方案可以返回:current-set 参数。它必须作为一个元素的序列返回。有很多方法可以做到这一点:

[current-set] ; as given - simplest
(list current-set)
(cons current-set ())
(conj () current-set)
...

如果您立即从subset-sum-helper 返回(无递归),您将看到向量

=> (subset-sum #{} 0)
[#{}]

否则你会看到一个由for生成的序列,它像一个列表一样打印出来:

=> (subset-sum (set (range 1 10)) 7)
(#{1 2 4}
 #{1 2 4}
 #{1 6}
 #{1 2 4}
 #{1 2 4}
 #{2 5}
 #{3 4}
 #{1 2 4}
 #{1 2 4}
 #{3 4}
 #{2 5}
 #{1 6}
 #{7})

当无法回答时,subset-sum-helper 返回一个空序列:

=> (subset-sum-helper #{2 4 6} #{} 19)
()

再一次,它被打印成一个列表。

算法有问题:

  • 它多次找到每个解决方案 - (count s) 的阶乘解决方案 s
  • 如果采用的元素elem 超过目标,它 毫无用处地尝试添加 remaining 集合的每个排列。

如果我们稍微改写一下,代码会更容易理解。

subset-sum-helper 的递归调用完整地传递了第一个和第三个参数。如果我们使用letfn 使这个函数成为subset-sum 的本地函数,我们可以不使用这些参数:它们是从上下文中提取的。现在看起来像这样:

(defn subset-sum [a-set target]
  (letfn [(subset-sum-helper [current-set]
             (if (= (reduce + current-set) target)
                 [current-set]
                 (let [remaining (clojure.set/difference a-set current-set)]
                         (for [elem remaining
                               solution (subset-sum-helper (conj current-set elem))]
                             solution))))]
      (subset-sum-helper #{})))

...对sum 函数的单个调用已被内联扩展。

现在很清楚 subset-sum-helper 正在返回包含其单个 current-set 参数的解决方案。 for 表达式正在枚举current-seta-set not 中的每个元素elem,包含当前集合和元素的解决方案。它正在为所有这些元素连续执行此操作。因此,从所有解决方案都包含的空集开始,它会生成所有解决方案。

【讨论】:

    【解决方案2】:

    也许这个解释对你有帮助:

    首先,我们可以在最少的代码中试验for 函数的预期行为(带和不带括号),但删除与递归相关的代码

    带括号:

    (for [x #{1 2 3}
          y [#{x}]]
      y)
    => (#{1} #{2} #{3})
    

    不带括号:

    (for [x #{1 2 3}
          y #{x}]
      y)
    => (1 2 3)
    

    带括号和括号内的更多元素*:**

    (for [x #{1 2 3}
          y [#{x}  :a :b :c]]
      y)
    => (#{1} :a :b :c #{2} :a :b :c #{3} :a :b :c)
    

    所以你需要(在这种情况下)括号以避免迭代集合。

    如果我们不使用方括号,我们将使用“x”作为 y 的绑定值,如果使用方括号,我们将使用 #{x} 作为 y 的绑定值。

    换句话说,代码作者需要一个集合,而不是迭代一个集合作为其 for 中的绑定值。所以她把一个集合放到一个序列“[#{x}]”中

    总结
    "for" 函数接受一个或多个 binding-form/collection-expr 对的向量 因此,如果您的“collection-expre”是#{:a},则迭代结果将为 (:a) 但如果您的“collection-expre”为 [#{:a}],则迭代结果将为 (#{:a} )

    对不起,我的解释多余,但这些细微差别很难说清楚

    【讨论】:

    • 这个解释帮助我发现了我的思维错误。我将(let [foo #{x y z}] ... ) 中的binding 行为与(for [foo #{x y z}] ... ) 中的iterating 行为混淆了。两者的语法相同,但行为却截然不同!
    【解决方案3】:

    只是为了好玩,这里有一个更干净的解决方案,仍然使用for

    (defn subset-sum [s target]
      (cond
        (neg? target) ()
        (zero? target) (list #{})
        (empty? s) ()
        :else (let [f (first s), ns (next s)]
                (lazy-cat
                  (for [xs (subset-sum ns (- target f))] (conj xs f))
                  (subset-sum ns target)))))
    

    【讨论】:

      猜你喜欢
      • 2021-01-27
      • 2021-04-12
      • 2012-08-31
      • 1970-01-01
      • 2013-03-20
      • 2021-11-16
      • 1970-01-01
      • 2017-07-06
      • 2019-11-23
      相关资源
      最近更新 更多