`for`如何在这个递归的Clojure代码中工作?

时间:2014-03-07 07:56:10

标签: for-loop recursion clojure list-comprehension

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。”

我非常感谢你能给我的任何帮助。谢谢!

3 个答案:

答案 0 :(得分:3)

subset-sum-helper函数始终返回解决方案的序列。如果未满足targetsolution表达式末尾的for正文将枚举此类序列。当满足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)的{​​{1}}次因数。
  • 如果采用的元素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-setfor 而非的每个元素elema-set表达式枚举包含当前集和元素的解法。它正在为所有这些元素连续地做这件事。因此,从所有解决方案包含的空集开始,它会生成所有解决方案。

答案 1 :(得分: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} )

很抱歉我的解释很冗余,但很难用这些细微差别来表达

答案 2 :(得分:1)

只是为了好玩,这是一个更清洁的解决方案,仍在使用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)))))