fun pp(amt)=
let
val c_50=ref 0;
val c_10=ref 0;
val c_x=ref 0
in
while !c_50 <= (amt div 500) do
(while !c_10 <= (amt div 100) do
(if ((500 * !c_50) + (100 * !c_10)) = amt
then print("500won =>" ^ Int.toString(!c_50) ^
" 100won =>" ^ Int.toString(!c_10) ^ "\n")
else
c_x := !c_x +1 -1;
c_10 := !c_10+1);
c_50 := !c_50+1)
end;
我想在SML中打印所有可能的更改组合,但c_50不会更改。
例如,如果我运行pp(500)
,我想要以下输出:
500won =&gt; 0 100won =&gt; 5
500won =&gt; 1 100won =&gt; 0
但现在我的程序只打印
500won =&gt; 0 100won =&gt; 500
结束了。我该如何解决这个问题?
答案 0 :(得分:2)
当递归是一个选项时,请避免使用while
和ref
等命令式结构。
让您的功能可读。功能名称和变量名称不清楚它应该做什么。其中一些评论也没有受到伤害。
将问题划分为更容易解决的子部分。 (当你已经弄清楚如何解决它时,这通常会更容易,所以在初步尝试之后重写你的解决方案是完全没问题的。)
让我们将问题表示为val allPossiblePayments : int -> int list -> int list list
,以便allPossiblePayments sum coins
列出从coins
总和到sum
的所有可能的硬币付款方式。我们可以假设sum >= 0
并且所有硬币都是正数,或者我们可能会使函数变得健壮并检查它。
fun allPossiblePayments sum coins =
if sum < 0 then raise Fail "Negative amount" else
if List.exists (coin => coin <= 0) cs then raise Fail "Non-positive coin value"
else allPossible sum coins (* non-robust version *)
在我们开始之前,我们可能想要准备一些测试。
无论可用的硬币是什么,都有一种方法可以支付零:空袋硬币;
val allPossiblePayments_1 = allPossiblePayments 0 [1, 2, 5] = [[]]
所有硬币除以总金额的特殊情况:
val allPossiblePayments_2 = allPossiblePayments 5 [1, 2, 5]
(* should contain [5], [2, 2, 1], [2, 1, 1, 1] and [1, 1, 1, 1, 1] *)
greedy choice不是选项的情况:
val allPossiblePayments_3 = allPossiblePayments 11 [2, 5]
(* should contain [[5, 2, 2, 2]] *)
我没有表达allPossiblePayments_2
和allPossiblePayments_3
作为正确测试(返回bool或抛出异常)的原因是它有点困难:我们没有&#39 ;如果&#34;付款选项&#34;包括相同硬币的不同排序(排列;例如[1, 2]
和[2, 1]
被区别对待,两者都应包含在结果中)或不包括(组合;例如[1, 2]
和[2, 1]
被视为相同,并且结果中只应包含其中一个)。组合可能是正确的选择。因此,测试这些函数本身就会成为一个小问题(比较排列/组合)。
即使硬币的相同组合会以这种方式出现多次,但它更简单一些。
(* putInFront x [[a,b,c],[d,e,f],[g,h,i]] = [[x,a,b,c],[x,d,e,f],[x,g,h,i]] *)
fun putInFront coin (subResult::subResults) = (coin::subResult) :: putInFront coin subResults
| putInFront x [] = []
fun allPossible 0 coins = [[]]
| allPossible sum coins =
let val validCoins = List.filter (fn coin => coin <= sum) coins
val subResults = List.map (fn coin => (coin, allPossible (sum-coin) validCoins)) validCoins
val totResults = List.map (fn (coin, subResult) => putInFront coin subResult) subResults
in List.concat totResults end
代码说明:
sum
,我们只会查看小于validCoins
的{{1}}。sum
中的每一个,我们形成一对coin
,它是通过递归地解决此问题来计算的,但是对于较小的量(coin, subResult)
。 (另外,如果某枚硬币对于sum-coin
来说太大,那么它也适用于sum
。)sum-coin
,大小为(coin, subResult)
,sum-coin
的子问题的解决方案可以解决大小问题subResult
每个子结果并在其前面添加sum
。coin
都有subResult
类型,并且我们有的列表,因此我们将列表展平一次。 (这意味着对于每次递归调用,当 sum 变小时,我们会将问题分解为许多结果列表(每个硬币一个)并将它们合并到一个结果列表中。测试此功能,
int list list
我们发现所有这些都是有效的,但有些是其他人的排列。
只获得组合可能有点棘手。一种不好的方法是生成所有排列,对它们进行排序然后删除重复项。这很糟糕,因为我们产生了不必要的结果,只是为了花更多的精力再次摆脱它们。
更好的方法是按排序顺序生成它们,并在生成它们时排除重复项。如果一个硬币大于或等于该结果中的最大硬币,则可以通过在结果 的头部添加硬币来实现此目的。
- allPossible 5 [1,2,5];
val it = [[1,1,1,1,1],[1,1,1,2],[1,1,2,1],[1,2,1,1]
,[1,2,2],[2,1,1,1],[2,1,2],[2,2,1],[5]] : int list list
使用此版本的fun putInFront _ [] = []
| putInFront validCoin ((coin::subResult)::subResults) =
if validCoin >= coin
then (validCoin::coin::subResult) :: putInFront validCoin subResults
else putInFront validCoin subResults
| putInFront validCoin ([]::subResults) = [validCoin] :: putInFront validCoin subResults
进行测试,而不是使用旧版本
putInFront
我们必须抛弃所有- allPossible 5 [5,2,1];
val it = [[5],[2,2,1],[2,1,1,1],[1,1,1,1,1]] : int list list
,这一点并不十分明显。另一种选择可能是在(validCoin::coin::subResult)
时只丢弃validCoin
并保留coin::subResult
。如果我们这样做,我们会得到一些时髦的部分结果,
validCoin < coin
相反,有人可能会说,如果- allPossible 5 [5,2,1];
val it = [[5],[2,2,1],[2,2],[2,1,1,1],[2,2],[2,1,1],[2,1],[2],[1,1,1,1,1]] : int list list
,其中validCoin < coin
是某个硬币而validCoin
是部分结果coin
中最大的硬币,那么解决方案{{已包含{1}}已被计算,此次coin::subResult
可被丢弃。
以上是上述代码的清理版本,以及一些解释和想法;
validCoin
validCoin
确实会过滤然后映射,因此会将这些内容合并到fun putInFront c =
List.mapPartial
(fn cs => case cs of
[] => SOME [c]
| (cmax::_) => if c >= cmax then SOME (c::cs) else NONE)
fun concatMap f xs = List.concat (List.map f xs)
fun allPossible 0 _ = [[]]
| allPossible sum coins =
let val validCoins = List.filter (fn coin => coin <= sum) coins
in concatMap (fn coin => putInFront coin (allPossible (sum-coin) validCoins)) validCoins end
fun allPossiblePayments n cs =
if n < 0 then raise Fail "Cannot handle negative amounts" else
if List.exists (fn c => c <= 0) cs then raise Fail "Cannot handle non-positive coin values"
else allPossible n cs
中,当其辅助函数返回putInFront
过滤掉该元素时,List.mapPartial
包括NONE
{1}}作为结果的一部分。SOME y
简化为y
,而不是撰写fun putInFront c subResults = List.mapPartial (fn ...) subResults
。fun putInFront c = List.mapPartial (fn ...)
中,当返回putInFront
时,SOME (c::cs)
实际上包含cs
作为首要元素,因此我们不需要像我们在以前的版本。这样可以节省不断重建非优化SML编译器的列表。cmax
生成多个结果,然后使用List.map
将这些结果列表连接到一个列表中,这是常见的事情,您会发现List.concat
到concatMap
是两个人的方便收缩。(coin, subResult)
对实际上是多余的,因为我们之后将这两个对直接传递到putInFront
。但是,通常可以考虑一次以较小的步骤解决问题。putInFront
和allPossible
,这样它们只对allPossiblePayments
可见,例如使用local ... in ... end
,但您也可以假装计划将这些功能放在具有strong ascription的模块中。