找到sml中所有可能的变化组合

时间:2016-05-23 16:36:23

标签: sml

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

结束了。我该如何解决这个问题?

1 个答案:

答案 0 :(得分:2)

一些一般性建议

  1. 当递归是一个选项时,请避免使用whileref等命令式结构。

  2. 让您的功能可读。功能名称和变量名称不清楚它应该做什么。其中一些评论也没有受到伤害。

  3. 将问题划分为更容易解决的子部分。 (当你已经弄清楚如何解决它时,这通常会更容易,所以在初步尝试之后重写你的解决方案是完全没问题的。)

  4. 一些具体建议

    让我们将问题表示为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_2allPossiblePayments_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
    

    代码说明:

    1. 对于任何金额sum,我们只会查看小于validCoins的{​​{1}}。
    2. 对于这些有效硬币sum中的每一个,我们形成一对coin,它是通过递归地解决此问题来计算的,但是对于较小的量(coin, subResult)。 (另外,如果某枚硬币对于sum-coin来说太大,那么它也适用于sum。)
    3. 对于每对sum-coin,大小为(coin, subResult)sum-coin的子问题的解决方案可以解决大小问题subResult每个子结果并在其前面添加sum
    4. 由于每个coin都有subResult类型,并且我们有列表,因此我们将列表展平一次。 (这意味着对于每次递归调用,当 sum 变小时,我们会将问题分解为许多结果列表(每个硬币一个)并将它们合并到一个结果列表中。
    5. 测试此功能,

      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
      1. 由于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}}作为结果的一部分。
      2. 可以使用Eta-conversionSOME y简化为y,而不是撰写fun putInFront c subResults = List.mapPartial (fn ...) subResults
      3. 一件小事;在fun putInFront c = List.mapPartial (fn ...)中,当返回putInFront时,SOME (c::cs)实际上包含cs作为首要元素,因此我们不需要像我们在以前的版本。这样可以节省不断重建非优化SML编译器的列表。
      4. 由于使用cmax生成多个结果,然后使用List.map将这些结果列表连接到一个列表中,这是常见的事情,您会发现List.concatconcatMap是两个人的方便收缩。
      5. 创建(coin, subResult)对实际上是多余的,因为我们之后将这两个对直接传递到putInFront。但是,通常可以考虑一次以较小的步骤解决问题。
      6. 您可以移动putInFrontallPossible,这样它们只对allPossiblePayments可见,例如使用local ... in ... end,但您也可以假装计划将这些功能放在具有strong ascription的模块中。