【问题标题】:Solving the NP-complete problem in XKCD解决 XKCD 中的 NP 完全问题
【发布时间】:2010-09-13 14:18:01
【问题描述】:

有问题的问题/漫画:http://xkcd.com/287/

我不确定这是不是最好的方法,但这是我迄今为止想出的。我正在使用 CFML,但它应该是任何人都可以阅读的。

<cffunction name="testCombo" returntype="boolean">
    <cfargument name="currentCombo" type="string" required="true" />
    <cfargument name="currentTotal" type="numeric" required="true" />
    <cfargument name="apps" type="array" required="true" />

    <cfset var a = 0 />
    <cfset var found = false />

    <cfloop from="1" to="#arrayLen(arguments.apps)#" index="a">
        <cfset arguments.currentCombo = listAppend(arguments.currentCombo, arguments.apps[a].name) />
        <cfset arguments.currentTotal = arguments.currentTotal + arguments.apps[a].cost />
        <cfif arguments.currentTotal eq 15.05>
            <!--- print current combo --->
            <cfoutput><strong>#arguments.currentCombo# = 15.05</strong></cfoutput><br />
            <cfreturn true />
        <cfelseif arguments.currentTotal gt 15.05>
            <cfoutput>#arguments.currentCombo# > 15.05 (aborting)</cfoutput><br />
            <cfreturn false />
        <cfelse>
            <!--- less than 15.05 --->
            <cfoutput>#arguments.currentCombo# < 15.05 (traversing)</cfoutput><br />
            <cfset found = testCombo(arguments.currentCombo, arguments.currentTotal, arguments.apps) />
        </cfif>
    </cfloop>
</cffunction>

<cfset mf = {name="Mixed Fruit", cost=2.15} />
<cfset ff = {name="French Fries", cost=2.75} />
<cfset ss = {name="side salad", cost=3.35} />
<cfset hw = {name="hot wings", cost=3.55} />
<cfset ms = {name="moz sticks", cost=4.20} />
<cfset sp = {name="sampler plate", cost=5.80} />
<cfset apps = [ mf, ff, ss, hw, ms, sp ] />

<cfloop from="1" to="6" index="b">
    <cfoutput>#testCombo(apps[b].name, apps[b].cost, apps)#</cfoutput>
</cfloop>

上面的代码告诉我,加起来 15.05 美元的唯一组合是 7 份混合水果,我的 testCombo 函数需要执行 232 次才能完成。

是否有更好的算法来得出正确的解决方案?我找到了正确的解决方案吗?

【问题讨论】:

  • 您缺少 1 个采样器、2 个热翅、1 个混合水果。
  • 这种语言令人憎恶。就像VB和XML一样决定生孩子。
  • “有没有更好的算法来得出正确的解决方案?” - 这是计算机科学中最大的未解决问题之一!如果stackoverflow提出了一个通用的解决方案,我会印象深刻的:)
  • 两个混蛋的混蛋。 +1 保罗巴图姆。
  • @Paul Batum:这让我很开心。正是我的感受。

标签: language-agnostic np-complete


【解决方案1】:

它提供了解决方案的所有排列,但我认为我在代码大小上击败了其他所有人。

item(X) :- member(X,[215, 275, 335, 355, 420, 580]).
solution([X|Y], Z) :- item(X), plus(S, X, Z), Z >= 0, solution(Y, S).
solution([], 0).

使用 swiprolog 的解决方案:

?- solution(X, 1505).

X = [215, 215, 215, 215, 215, 215, 215] ;

X = [215, 355, 355, 580] ;

X = [215, 355, 580, 355] ;

X = [215, 580, 355, 355] ;

X = [355, 215, 355, 580] ;

X = [355, 215, 580, 355] ;

X = [355, 355, 215, 580] ;

X = [355, 355, 580, 215] ;

X = [355, 580, 215, 355] ;

X = [355, 580, 355, 215] ;

X = [580, 215, 355, 355] ;

X = [580, 355, 215, 355] ;

X = [580, 355, 355, 215] ;

No

【讨论】:

  • 这就是你可以用声明性语言做的事情。爱它。 PROLOG 很棒。
【解决方案2】:

关于 NP 完全问题的要点不是它在小数据集上很棘手,而是解决它的工作量以大于多项式的速度增长,即没有 O(n^x) 算法.

如果时间复杂度是 O(n!),就像(我相信)上面提到的两个问题,那就是 NP。

【讨论】:

    【解决方案3】:

    递归不是更优雅吗(在 Perl 中)?

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    my @weights  = (2.15, 2.75, 3.35, 3.55, 4.20, 5.80);
    
    my $total = 0;
    my @order = ();
    
    iterate($total, @order);
    
    sub iterate
    {
        my ($total, @order) = @_;
        foreach my $w (@weights)
        {
            if ($total+$w == 15.05)
            {
                print join (', ', (@order, $w)), "\n";
            }
            if ($total+$w < 15.05)
            {
                iterate($total+$w, (@order, $w));
            }
        }
    }
    

    输出

    marco@unimatrix-01:~$ ./xkcd-knapsack.pl
    2.15, 2.15, 2.15, 2.15, 2.15, 2.15, 2.15
    2.15, 3.55, 3.55, 5.8
    2.15, 3.55, 5.8, 3.55
    2.15, 5.8, 3.55, 3.55
    3.55, 2.15, 3.55, 5.8
    3.55, 2.15, 5.8, 3.55
    3.55, 3.55, 2.15, 5.8
    3.55, 5.8, 2.15, 3.55
    5.8, 2.15, 3.55, 3.55
    5.8, 3.55, 2.15, 3.55

    【讨论】:

    • 除了列出相同的答案 9 次
    • 你需要在计算出答案后在其中添加一个排序,这样你就可以去除欺骗了。
    • 比PROLOG 3 liner更优雅?你在开玩笑吗?
    • 修复它以显示实际订单(而不是价值!),你就会被投票!
    【解决方案4】:

    虽然背包是NP完全的,但它是一个非常特殊的问题:它通常的动态程序实际上是优秀的(http://en.wikipedia.org/wiki/Knapsack_problem

    如果你做正确的分析,结果是 O(nW),n 是项目数,W 是目标数。问题是当你必须 DP 超过一个大的 W 时,这就是我们得到 NP 行为的时候。但在大多数情况下,背包的表现相当不错,您可以毫无问题地解决非常大的实例。就 NP 完全问题而言,背包是最简单的问题之一。

    【讨论】:

    • 这个特定版本也简化为 O(n * lcm(numbers))。如果你的麻袋相当好,那可能会小于 W。
    【解决方案5】:

    这里是使用 constraint.py 的解决方案

    >>> from constraint import *
    >>> problem = Problem()
    >>> menu = {'mixed-fruit': 2.15,
    ...  'french-fries': 2.75,
    ...  'side-salad': 3.35,
    ...  'hot-wings': 3.55,
    ...  'mozarella-sticks': 4.20,
    ...  'sampler-plate': 5.80}
    >>> for appetizer in menu:
    ...    problem.addVariable( appetizer, [ menu[appetizer] * i for i in range( 8 )] )
    >>> problem.addConstraint(ExactSumConstraint(15.05))
    >>> problem.getSolutions()
    [{'side-salad': 0.0, 'french-fries': 0.0, 'sampler-plate': 5.7999999999999998, 'mixed-fruit': 2.1499999999999999, 'mozarella-sticks': 0.0, 'hot-wings': 7.0999999999999996},
     {'side-salad': 0.0, 'french-fries': 0.0, 'sampler-plate': 0.0, 'mixed-fruit':     15.049999999999999, 'mozarella-sticks': 0.0, 'hot-wings': 0.0}]
    

    所以解决方案是订购一个采样盘、一个混合水果和 2 个热翅,或者订购 7 个混合水果。

    【讨论】:

      【解决方案6】:

      这是一个使用 F# 的解决方案:

      #light
      
      type Appetizer = { name : string; cost : int }
      
      let menu = [
          {name="fruit"; cost=215}
          {name="fries"; cost=275}
          {name="salad"; cost=335}
          {name="wings"; cost=355}
          {name="moz sticks"; cost=420}
          {name="sampler"; cost=580}
          ] 
      
      // Choose: list<Appetizer> -> list<Appetizer> -> int -> list<list<Appetizer>>
      let rec Choose allowedMenu pickedSoFar remainingMoney =
          if remainingMoney = 0 then
              // solved it, return this solution
              [ pickedSoFar ]
          else
              // there's more to spend
              [match allowedMenu with
               | [] -> yield! []  // no more items to choose, no solutions this branch
               | item :: rest -> 
                  if item.cost <= remainingMoney then
                      // if first allowed is within budget, pick it and recurse
                      yield! Choose allowedMenu (item :: pickedSoFar) (remainingMoney - item.cost)
                  // regardless, also skip ever picking more of that first item and recurse
                  yield! Choose rest pickedSoFar remainingMoney]
      
      let solutions = Choose menu [] 1505
      
      printfn "%d solutions:" solutions.Length 
      solutions |> List.iter (fun solution ->
          solution |> List.iter (fun item -> printf "%s, " item.name)
          printfn ""
      )
      
      (*
      2 solutions:
      fruit, fruit, fruit, fruit, fruit, fruit, fruit,
      sampler, wings, wings, fruit,
      *)
      

      【讨论】:

        【解决方案7】:

        阅读Knapsack Problem

        【讨论】:

        • 这是一个不同的问题,背包问题假设不精确的解决方案,并且选择没有重复。
        • 那为什么餐厅老板要给服务员提供关于背包问题的文章呢?
        • 可能是因为它是卡通片。
        • 因为问题密切相关。解决背包问题也能解决这个问题。
        【解决方案8】:

        您现在已经获得了所有正确的组合,但您仍然检查了比您需要的更多的东西(正如您的结果显示的许多排列所证明的那样)。此外,您将省略最后一个达到 15.05 标记的项目。

        我有一个 PHP 版本,它执行 209 次递归调用迭代(如果我得到所有排列,它会执行 2012 年)。如果在循环结束之前取出刚刚检查的项目,则可以减少计数。

        我不知道 CF 语法,但它会是这样的:

                <cfelse>
                    <!--- less than 15.50 --->
                    <!--<cfoutput>#arguments.currentCombo# < 15.05 (traversing)</cfoutput><br />-->
                    <cfset found = testCombo(CC, CT, arguments.apps) />
                ------- remove the item from the apps array that was just checked here ------
            </cfif>
        </cfloop>
        

        编辑:作为参考,这是我的 PHP 版本:

        <?
          function rc($total, $string, $m) {
            global $c;
        
            $m2 = $m;
            $c++;
        
            foreach($m as $i=>$p) {
              if ($total-$p == 0) {
                print "$string $i\n";
                return;
              }
              if ($total-$p > 0) {
                rc($total-$p, $string . " " . $i, $m2);
              }
              unset($m2[$i]);
            }
          }
        
          $c = 0;
        
          $m = array("mf"=>215, "ff"=>275, "ss"=>335, "hw"=>355, "ms"=>420, "sp"=>580);
          rc(1505, "", $m);
          print $c;
        ?>
        

        输出

         mf mf mf mf mf mf mf
         mf hw hw sp
        209
        

        编辑 2:

        由于解释为什么可以删除元素需要的时间比我在评论中所能容纳的要多一些,所以我在这里添加它。

        基本上,每次递归都会找到包含当前搜索元素的所有组合(例如,第一步将找到包括至少一个混合水果在内的所有组合)。理解它的最简单方法是跟踪执行,但由于这会占用大量空间,所以我会按照目标是 6.45 来执行。

        MF (2.15)
          MF (4.30)
            MF (6.45) *
            FF (7.05) X
            SS (7.65) X
            ...
          [MF removed for depth 2]
          FF (4.90)
            [checking MF now would be redundant since we checked MF/MF/FF previously]
            FF (7.65) X
            ...
          [FF removed for depth 2]
          SS (5.50)
          ...
        [MF removed for depth 1]
        

        此时,我们已经检查了所有包含混合水果的组合,因此无需再次检查混合水果。您也可以使用相同的逻辑在每个更深的递归中修剪数组。

        像这样跟踪它实际上可以节省一点时间——知道价格是从低到高排序的,这意味着我们一旦超过目标就不需要继续检查项目。

        【讨论】:

        • 我不确定为什么可以从数组中删除该项目。在我看来,在这种情况下没关系,但不一定适用于所有情况。你能再解释一下吗?
        • 好的,我知道删除元素现在有什么帮助了。看起来你(有效地)通过 val,而不是通过 ref。真的吗?我一直在传递 ref 以保留内存并缩短运行时间,但是减少递归的权衡可能会通过删除值获得胜利。 (我会安排时间看看。)
        • 是的,你是对的,它本质上是按值传递(我很懒)。您可以通过传递最小索引来检查递归函数而不是修改后的数组来改进这一点。
        【解决方案9】:

        我会就算法本身的设计提出一个建议(这就是我解释您最初问题的意图的方式)。这是我写的解决方案的片段

        ....
        
        private void findAndReportSolutions(
            int target,  // goal to be achieved
            int balance, // amount of goal remaining
            int index    // menu item to try next
        ) {
            ++calls;
            if (balance == 0) {
                reportSolution(target);
                return; // no addition to perfect order is possible
            }
            if (index == items.length) {
                ++falls;
                return; // ran out of menu items without finding solution
            }
            final int price = items[index].price;
            if (balance < price) {
                return; // all remaining items cost too much
            }
            int maxCount = balance / price; // max uses for this item
            for (int n = maxCount; 0 <= n; --n) { // loop for this item, recur for others
                ++loops;
                counts[index] = n;
                findAndReportSolutions(
                    target, balance - n * price, index + 1
                );
            }
        }
        
        public void reportSolutionsFor(int target) {
            counts = new int[items.length];
            calls = loops = falls = 0;
            findAndReportSolutions(target, target, 0);
            ps.printf("%d calls, %d loops, %d falls%n", calls, loops, falls);
        }
        
        public static void main(String[] args) {
            MenuItem[] items = {
                new MenuItem("mixed fruit",       215),
                new MenuItem("french fries",      275),
                new MenuItem("side salad",        335),
                new MenuItem("hot wings",         355),
                new MenuItem("mozzarella sticks", 420),
                new MenuItem("sampler plate",     580),
            };
            Solver solver = new Solver(items);
            solver.reportSolutionsFor(1505);
        }
        
        ...
        

        (请注意,构造函数确实通过增加价格对菜单项进行排序,以在剩余余额小于任何剩余菜单项时启用恒定时间提前终止。)

        样本运行的输出是:

        7 mixed fruit (1505) = 1505
        1 mixed fruit (215) + 2 hot wings (710) + 1 sampler plate (580) = 1505
        348 calls, 347 loops, 79 falls
        

        我想强调的设计建议是,在上面的代码中,findAndReportSolution(...) 的每个嵌套(递归)调用都处理一个由 index 参数标识的菜单项的数量。换句话说,递归嵌套与内联嵌套循环的行为相似。最外层计算第一个菜单项的可能使用,下一个计算第二个菜单项的使用,等等。(当然,递归的使用将代码从对特定数量的菜单项的依赖中解放出来!)

        我建议这样可以更容易地设计代码,并且更容易理解每​​个调用正在做什么(考虑到特定项目的所有可能用途,将菜单的其余部分委托给从属调用)。它还避免了产生多项目解决方案的所有排列的组合爆炸(如上述输出的第二行,它只出现一次,而不是重复出现不同排序的项目)。

        我试图最大化代码的“显而易见性”,而不是试图最小化某些特定方法的调用次数。例如,上述设计让委托调用确定是否已达到解决方案,而不是在调用点周围进行检查,这样会减少调用次数,但会导致代码混乱。

        【讨论】:

          【解决方案10】:

          嗯,你知道什么是奇怪的。解决方案是菜单上第一项中的七个。

          既然这显然意味着要在短时间内用纸和铅笔解决,为什么不将订单总额除以每件商品的价格,看看他们是否有机会订购了一件商品的倍数?

          例如,

          15.05/2.15 = 7 种混合水果 15.05/2.75 = 5.5 个炸薯条。

          然后继续进行简单的组合...

          15 / (2.15 + 2.75) = 3.06122449 混合水果和炸薯条。

          换句话说,假设该解决方案应该是简单的,并且可以由人类解决,而无需使用计算机。然后测试最简单、最明显(因此隐藏在显而易见的地方)的解决方案是否有效。

          我发誓这个周末我在俱乐部关门后的凌晨 4:30 点了价值 4.77 美元的开胃菜(含税)时,我会去当地的康尼吃这个。

          【讨论】:

            【解决方案11】:

            在python中。
            我对“全局变量”有一些问题,所以我把函数作为对象的方法。它是递归的,它为漫画中的问题调用了 29 次,在第一次成功匹配时停止

            class Solver(object):
                def __init__(self):
                    self.solved = False
                    self.total = 0
                def solve(s, p, pl, curList = []):
                    poss = [i for i in sorted(pl, reverse = True) if i <= p]
                    if len(poss) == 0 or s.solved:
                        s.total += 1
                        return curList
                    if abs(poss[0]-p) < 0.00001:
                        s.solved = True # Solved it!!!
                        s.total += 1
                        return curList + [poss[0]]
                    ml,md = [], 10**8
                    for j in [s.solve(p-i, pl, [i]) for i in poss]:
                        if abs(sum(j)-p)<md: ml,md = j, abs(sum(j)-p)
                    s.total += 1
                    return ml + curList
            
            
            priceList = [5.8, 4.2, 3.55, 3.35, 2.75, 2.15]
            appetizers = ['Sampler Plate', 'Mozzarella Sticks', \
                          'Hot wings', 'Side salad', 'French Fries', 'Mixed Fruit']
            
            menu = zip(priceList, appetizers)
            
            sol = Solver()
            q = sol.solve(15.05, priceList)
            print 'Total time it runned: ', sol.total
            print '-'*30
            order = [(m, q.count(m[0])) for m in menu if m[0] in q]
            for o in order:
                print '%d x %s \t\t\t (%.2f)' % (o[1],o[0][1],o[0][0])
            
            print '-'*30
            ts = 'Total: %.2f' % sum(q)
            print ' '*(30-len(ts)-1),ts
            

            输出:

            Total time it runned:  29
            ------------------------------
            1 x Sampler Plate   (5.80)
            2 x Hot wings       (3.55)
            1 x Mixed Fruit       (2.15)
            ------------------------------
                           Total: 15.05
            

            【讨论】:

              【解决方案12】:

              实际上,我对算法进行了更多重构。我遗漏了几个正确的组合,这是因为我在成本超过 15.05 时立即返回 - 我没有费心检查我可以添加的其他(更便宜的)项目。这是我的新算法:

              <cffunction name="testCombo" returntype="numeric">
                  <cfargument name="currentCombo" type="string" required="true" />
                  <cfargument name="currentTotal" type="numeric" required="true" />
                  <cfargument name="apps" type="array" required="true" />
              
                  <cfset var a = 0 />
                  <cfset var found = false /> 
                  <cfset var CC = "" />
                  <cfset var CT = 0 />
              
                  <cfset tries = tries + 1 />
              
                  <cfloop from="1" to="#arrayLen(arguments.apps)#" index="a">
                      <cfset combos = combos + 1 />
                      <cfset CC = listAppend(arguments.currentCombo, arguments.apps[a].name) />
                      <cfset CT = arguments.currentTotal + arguments.apps[a].cost />
                      <cfif CT eq 15.05>
                          <!--- print current combo --->
                          <cfoutput><strong>#CC# = 15.05</strong></cfoutput><br />
                          <cfreturn true />
                      <cfelseif CT gt 15.05>
                          <!--<cfoutput>#arguments.currentCombo# > 15.05 (aborting)</cfoutput><br />-->
                      <cfelse>
                          <!--- less than 15.50 --->
                          <!--<cfoutput>#arguments.currentCombo# < 15.05 (traversing)</cfoutput><br />-->
                          <cfset found = testCombo(CC, CT, arguments.apps) />
                      </cfif>
                  </cfloop>
                  <cfreturn found />
              </cffunction>
              
              <cfset mf = {name="Mixed Fruit", cost=2.15} />
              <cfset ff = {name="French Fries", cost=2.75} />
              <cfset ss = {name="side salad", cost=3.35} />
              <cfset hw = {name="hot wings", cost=3.55} />
              <cfset ms = {name="moz sticks", cost=4.20} />
              <cfset sp = {name="sampler plate", cost=5.80} />
              <cfset apps = [ mf, ff, ss, hw, ms, sp ] />
              
              <cfset tries = 0 />
              <cfset combos = 0 />
              
              <cfoutput>
                  <cfloop from="1" to="6" index="b">
                      #testCombo(apps[b].name, apps[b].cost, apps)#
                  </cfloop>
                  <br />
                  tries: #tries#<br />
                  combos: #combos#
              </cfoutput>
              

              输出:

              Mixed Fruit,Mixed Fruit,Mixed Fruit,Mixed Fruit,Mixed Fruit,Mixed Fruit,Mixed Fruit = 15.05
              Mixed Fruit,hot wings,hot wings,sampler plate = 15.05
              Mixed Fruit,hot wings,sampler plate,hot wings = 15.05
              Mixed Fruit,sampler plate,hot wings,hot wings = 15.05
              false false false hot wings,Mixed Fruit,hot wings,sampler plate = 15.05
              hot wings,Mixed Fruit,sampler plate,hot wings = 15.05
              hot wings,hot wings,Mixed Fruit,sampler plate = 15.05
              hot wings,sampler plate,Mixed Fruit,hot wings = 15.05
              false false sampler plate,Mixed Fruit,hot wings,hot wings = 15.05
              sampler plate,hot wings,Mixed Fruit,hot wings = 15.05
              false
              tries: 2014
              combos: 12067
              

              我认为这可能有所有正确的组合,但我的问题仍然存在:有更好的算法吗?

              【讨论】:

              • 混合水果,热翅,热翅 = 15.05? 2.15 + 3.55 + 3.55 = 9.25
              • 正如我在回答中提到的,他没有打印每个组合中的最后一个元素。
              • 我修复了组合中最后一项未显示的错误,并更新了输出。仍在研究如何减少所需的迭代。
              【解决方案13】:

              @rcar's answer学习,后来又进行了一次重构,得到了以下内容。

              与我编写的许多东西一样,我已经从 CFML 重构为 CFScript,但代码基本相同。

              我在他的建议中添加了数组中的动态起点(而不是按值传递数组并更改其值以供将来递归使用),这使我得到了与他相同的统计数据(209 次递归,571 次组合价格检查(循环迭代)),然后通过假设数组将按成本排序——因为它是——并在我们超过目标价格时立即中断。中断后,我们减少到 209 次递归和 376 次循环迭代。

              算法还有哪些其他改进?

              function testCombo(minIndex, currentCombo, currentTotal){
                  var a = 0;
                  var CC = "";
                  var CT = 0;
                  var found = false;
              
                  tries += 1;
                  for (a=arguments.minIndex; a <= arrayLen(apps); a++){
                      combos += 1;
                      CC = listAppend(arguments.currentCombo, apps[a].name);
                      CT = arguments.currentTotal + apps[a].cost;
                      if (CT eq 15.05){
                          //print current combo
                          WriteOutput("<strong>#CC# = 15.05</strong><br />");
                          return(true);
                      }else if (CT gt 15.05){
                          //since we know the array is sorted by cost (asc),
                          //and we've already gone over the price limit,
                          //we can ignore anything else in the array
                          break; 
                      }else{
                          //less than 15.50, try adding something else
                          found = testCombo(a, CC, CT);
                      }
                  }
                  return(found);
              }
              
              mf = {name="mixed fruit", cost=2.15};
              ff = {name="french fries", cost=2.75};
              ss = {name="side salad", cost=3.35};
              hw = {name="hot wings", cost=3.55};
              ms = {name="mozarella sticks", cost=4.20};
              sp = {name="sampler plate", cost=5.80};
              apps = [ mf, ff, ss, hw, ms, sp ];
              
              tries = 0;
              combos = 0;
              
              testCombo(1, "", 0);
              
              WriteOutput("<br />tries: #tries#<br />combos: #combos#");
              

              【讨论】:

                【解决方案14】:

                这是 Clojure 中的并发实现。计算(items-with-price 15.05) 需要大约 14 次组合生成递归和大约 10 次可能性检查。我花了大约 6 分钟在我的 Intel Q9300 上计算 (items-with-price 100)

                这只会给出第一个找到的答案,或者nil(如果没有),因为这就是问题所在。为什么要做更多你被告知要做的工作;)?

                ;; np-complete.clj
                ;; A Clojure solution to XKCD #287 "NP-Complete"
                ;; By Sam Fredrickson
                ;;
                ;; The function "items-with-price" returns a sequence of items whose sum price
                ;; is equal to the given price, or nil.
                
                (defstruct item :name :price)
                
                (def *items* #{(struct item "Mixed Fruit" 2.15)
                               (struct item "French Fries" 2.75)
                               (struct item "Side Salad" 3.35)
                               (struct item "Hot Wings" 3.55)
                               (struct item "Mozzarella Sticks" 4.20)
                               (struct item "Sampler Plate" 5.80)})
                
                (defn items-with-price [price]
                  (let [check-count (atom 0)
                        recur-count (atom 0)
                        result  (atom nil)
                        checker (agent nil)
                        ; gets the total price of a seq of items.
                        items-price (fn [items] (apply + (map #(:price %) items)))
                        ; checks if the price of the seq of items matches the sought price.
                        ; if so, it changes the result atom. if the result atom is already
                        ; non-nil, nothing is done.
                        check-items (fn [unused items]
                                      (swap! check-count inc)
                                      (if (and (nil? @result)
                                               (= (items-price items) price))
                                        (reset! result items)))
                        ; lazily generates a list of combinations of the given seq s.
                        ; haven't tested well...
                        combinations (fn combinations [cur s]
                                       (swap! recur-count inc)
                                       (if (or (empty? s)
                                               (> (items-price cur) price))
                                         '()
                                         (cons cur
                                          (lazy-cat (combinations (cons (first s) cur) s)
                                                    (combinations (cons (first s) cur) (rest s))
                                                    (combinations cur (rest s))))))]
                    ; loops through the combinations of items, checking each one in a thread
                    ; pool until there are no more combinations or the result atom is non-nil.
                    (loop [items-comb (combinations '() (seq *items*))]
                      (if (and (nil? @result)
                               (not-empty items-comb))
                        (do (send checker check-items (first items-comb))
                            (recur (rest items-comb)))))
                    (await checker)
                    (println "No. of recursions:" @recur-count)
                    (println "No. of checks:" @check-count)
                    @result))
                

                【讨论】:

                  【解决方案15】:

                  如果您想要优化算法,最好按降序尝试价格。这样您就可以先用完剩余的金额,然后再看看如何填写剩余的金额。

                  此外,您可以使用数学计算每次开始时每种食物的最大数量,这样您就不会尝试超过 15.05 美元目标的组合。

                  这个算法只需要尝试 88 种组合就可以得到一个完整的答案,这看起来是迄今为止发布的最低的:

                  public class NPComplete {
                      private static final int[] FOOD = { 580, 420, 355, 335, 275, 215 };
                      private static int tries;
                  
                      public static void main(String[] ignore) {
                          tries = 0;
                          addFood(1505, "", 0);
                          System.out.println("Combinations tried: " + tries);
                      }
                  
                      private static void addFood(int goal, String result, int index) {
                          // If no more food to add, see if this is a solution
                          if (index >= FOOD.length) {
                              tries++;
                              if (goal == 0)
                                  System.out.println(tries + " tries: " + result.substring(3));
                              return;
                          }
                  
                          // Try all possible quantities of this food
                          // If this is the last food item, only try the max quantity
                          int qty = goal / FOOD[index];
                          do {
                              addFood(goal - qty * FOOD[index],
                                      result + " + " + qty + " * " + FOOD[index], index + 1);
                          } while (index < FOOD.length - 1 && --qty >= 0);
                      }
                  }
                  

                  这是显示两种解决方案的输出:

                  9 次尝试:1 * 580 + 0 * 420 + 2 * 355 + 0 * 335 + 0 * 275 + 1 * 215 88 次尝试:0 * 580 + 0 * 420 + 0 * 355 + 0 * 335 + 0 * 275 + 7 * 215 尝试的组合:88

                  【讨论】:

                    猜你喜欢
                    • 2022-07-21
                    • 2010-12-19
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-01-22
                    • 2023-03-03
                    • 2011-11-14
                    • 2011-04-23
                    • 2011-10-18
                    相关资源
                    最近更新 更多