繁体   English   中英

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

[英]How does `for` work in this recursive Clojure code?

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这种方式。”

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

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每个元素elem 它正在为所有这些元素连续地做这件事。 因此,从所有解决方案包含的空集开始,它会生成所有解决方案。

也许这个解释可以帮助你:

首先,我们可以在最小代码中试验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})

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM