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
。”
我非常感谢你能给我的任何帮助。谢谢!
答案 0 :(得分:3)
subset-sum-helper
函数始终返回解决方案的序列。如果未满足target
,solution
表达式末尾的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-set
中for
而非的每个元素elem
,a-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)))))