优化产品装配/拆卸

时间:2013-02-25 16:31:10

标签: python optimization python-3.2

我有一个包含物品的商店。每个项目都是一个组件(它是一个组件)或一个由各种组件组成的产品(但从不包含两个或更多相同的组件)。

现在,当我想要从商店购买产品时,有各种各样的场景:

  • 商店包含必要数量的商品。
  • 商店包含我可以组装产品的组件。
  • 商店包含与所需产品共享组件的产品。我可以拆卸它们并组装所需的物品。
  • 上述任意组合。

下面你可以看到我的代码(getAssemblyPath)。如果可能的话,它确实找到了组装所需项目的方法,但它没有优化组装路径。

我想以两种方式优化路径:

  • 首先,选择采用最少数量的装配/反汇编操作的路径。
  • 其次,如果有各种这样的路径,请选择在商店中留下最少量反汇编组件的路径。

现在,我完全失去了如何完成这项优化(我甚至不确定这是针对SO还是数学的问题)。

如何更改getAssemblyPath以符合我的优化要求?

到目前为止我的代码:

#! /usr/bin/python

class Component:
    def __init__ (self, name): self.__name = name

    def __repr__ (self): return 'Component {}'.format (self.__name)

class Product:
    def __init__ (self, name, components):
        self.__name = name
        self.__components = components

    @property
    def components (self): return self.__components

    def __repr__ (self): return 'Product {}'.format (self.__name)

class Store:
    def __init__ (self): self.__items = {}

    def __iadd__ (self, item):
        item, count = item
        if not item in self.__items: self.__items [item] = 0
        self.__items [item] += count
        return self

    @property
    def items (self): return (item for item in self.__items.items () )

    @property
    def products (self): return ( (item, count) for item, count in self.__items.items () if isinstance (item, Product) )

    @property
    def components (self): return ( (item, count) for item, count in self.__items.items () if isinstance (item, Component) )

    def getAssemblyPath (self, product, count):
        if product in self.__items:
            take = min (count, self.__items [product] )
            print ('Take {} of {}'.format (take, product) )
            count -= take
            if not count: return
        components = dict ( (comp, count) for comp in product.components)
        for comp, count in self.components:
            if comp not in components: continue
            take = min (count, components [comp] )
            print ('Take {} of {}'.format (take, comp) )
            components [comp] -= take
            if not components [comp]: del components [comp]
            if not components: return
        for prod, count in self.products:
            if prod == product: continue
            shared = set (prod.components) & set (components.keys () )
            dis = min (max (components [comp] for comp in shared), count)
            print ('Disassemble {} of {}.'.format (dis, prod) )
            for comp in shared:
                print ('Take {} of {}.'.format (dis, comp) )
                components [comp] -= take
                if not components [comp]: del components [comp]
                if not components: return
        print ('Missing components:')
        for comp, count in components.items ():
            print ('{} of {}.'.format (count, comp) )

c1 = Component ('alpha')
c2 = Component ('bravo')
c3 = Component ('charlie')
c4 = Component ('delta')

p1 = Product ('A', [c1, c2] )
p2 = Product ('B', [c1, c2, c3] )
p3 = Product ('C', [c1, c3, c4] )

store = Store ()
store += (c2, 100)
store += (c4, 100)
store += (p1, 100)
store += (p2, 100)
store += (p3, 10)
store.getAssemblyPath (p3, 20)

输出:

Take 10 of Product C
Take 10 of Component delta
Disassemble 10 of Product A.
Take 10 of Component alpha.
Disassemble 10 of Product B.
Take 10 of Component charlie.

哪个有效,但它不必要地反汇编产品A,因为产品B包含所需的组件alpha和charlie。

-

编辑:

回答Blckknght非常明智的问题:

  

当您说您想要“最少数量的装配/拆卸操作”时,您是指最小数量的商品,还是最少数量的不同产品?

“asm / disasm action”是组装或拆卸一种产品的行为,无论涉及多少组件。我正在寻找最少数量的触摸物品,无论它们是否与众不同。

  

也就是说,拆解产品A的20个比拆解产品A的10个和产品B的另外5个更好?

后者更接近最佳状态。

  

此外,您说您希望避免遗留许多组件,但在您当前的代码中,所有未被请求的产品使用的反汇编组件都将丢失。这是故意的(也就是说,你想丢掉其他组件),还是一个bug?

方法getAssemblyPath仅确定如何获取项目的路径。它没有碰到实际的商店。它决不会分配给self.__items。可以把它想象成一个函数,向商店发出一个订单,保留他在(中)未来必须做的事情,以便从他的商店中获得所需数量的所需商品。

-

编辑2:

解决此问题的第一个显而易见的方法(或至少对我来说很明显)是首先搜索那些与所需产品共享最大组件数量的产品,因为每次反汇编都会获得更多所需的组件。但遗憾的是,这并不一定能产生最佳路径。举个例子:

产品A由组分α,β,γ,δ,ε和ζ组成。

产品B由组分α,β,η,δ,ε和θ组成。

产品C由组分α,β,γ,ι,κ和λ组成。

产品D由组分μ,ν,ξ,δ,ε和ζ组成。

我们在A的商店0,B的100,C的100和D的100。我们要求A的10个。现在如果我们首先看A与A共享大部分组件的产品,我们将找到B.我们拆解B中的10个得到α,β,δ和ε各10个。但是我们需要拆解10个C(得到γ)和10个D(得到ζ)。这将是40个动作(30个拆卸和10个组装)。 但最好的方法是拆卸10个C和10个D(30个动作,20个拆卸和10个装配)。

-

编辑3:

你不需要发布python代码来赢得赏金。只是向我解释算法,并证明它确实产生了最佳路径,或者如果存在多个最佳路径之一。

3 个答案:

答案 0 :(得分:3)

以下是我将如何解决这个问题。我想为此编写代码,但我认为我没有时间。

您可以递归地找到最佳解决方案。创建一个表示零件存储状态和当前请求的数据结构。现在,对于您需要的每个部分,进行一系列递归调用,尝试各种方式来填充订单。关键是通过尝试填充订单的方式,您将完成部分工作,因此递归调用现在是同一问题的稍微简单的版本。

这是一个基于您的示例的具体示例。我们需要填写由组件c1,c3和c4组成的产品3(p3)的订单。我们的订单是p3的20,我们有10 p3库存,所以我们很容易填写p3的前10个订单。现在我们的订单是p3中的10个,但是我们可以将它看作c1中的10个,c3中的10个和c4中的10个的顺序。对于第一次递归调用,我们反汇编p1,并为单个c1填写订单并在商店中放置一个额外的c2;所以这个递归调用是c1中的9个,c3中的10个和c4中的10个,并且在商店中具有更新的可用性。对于第二次递归调用,我们反汇编p2,并填写c1和c4的订单,并将额外的c2放入商店;所以这个递归调用是c1中的9个,c3中的10个和c4中的9个,并且在商店中有更新的可用性。

由于每次调用都会减少问题,递归的一系列调用将终止。递归调用应该返回一个成本度量标准,该度量标准表示调用未能找到解决方案,或者表示找到的解决方案成本是多少;该功能通过选择成本最低的解决方案来选择最佳解决方案。

我不确定,但你可以通过记忆电话加快速度。 Python在3.x系列中有一个非常漂亮的内置新版本,functools.lru_cache();因为您将问题标记为“Python 3.2”,所以您可以使用它。

What is memoization and how can I use it in Python?

通过识别已经使用相同的参数调用函数,并且只返回与以前相同的解决方案,memoization工作。所以它是一个缓存映射参数的答案。如果参数包含非必要数据(例如存储中存在多少组件c2),则存储器不太可能工作。但是如果我们想象我们有产品p1和p9,并且p9包含组件c1和c9,那么为了我们的目的,拆卸p1或p9中的一个应该是等价的:它们具有相同的拆卸成本,并且它们都产生我们需要的组件(c1)和我们不需要的(c2或c9)。因此,如果我们得到正确的递归调用参数,当我们开始尝试p9时,memoization可以返回一个即时答案,并且它可以节省大量时间。

嗯,既然我想到了,我们可能无法使用functools.lru_cache(),但我们可以自己记忆。我们可以创建解决方案的缓存:将元组映射到值的字典,以及构建只具有我们想要缓存的参数的元组。然后在我们的函数中,我们要做的第一件事就是检查解决方案的缓存,如果这个调用等同于缓存解决方案,那就返回它。

编辑:这是我到目前为止编写的代码。我还没有完成调试,所以它可能还没有产生正确的答案(我不确定,因为它需要很长时间,我还没有让它完成运行)。这个版本正在传递字典,这对于我关于记忆的想法不会很好,但我希望得到一个简单的版本,然后担心加速它。

此外,此代码拆分产品并将它们作为组件添加到商店,因此最终的解决方案首先会说“拆开10个产品A”,然后它会说“Take 20 component alpha”或者其他什么。换句话说,组件数量可能被认为很高,因为它不区分商店中已有的组件和通过拆卸产品放在那里的组件。

我现在没时间了,暂时不会工作,对不起。

#!/usr/bin/python3

class Component:
    def __init__ (self, name): self.__name = name

    #def __repr__ (self): return 'Component {}'.format (self.__name)
    def __repr__ (self): return 'C_{}'.format (self.__name)

class Product:
    def __init__ (self, name, components):
        self.__name = name
        self.__components = components

    @property
    def components (self): return self.__components

    #def __repr__ (self): return 'Product {}'.format (self.__name)
    def __repr__ (self): return 'P_{}'.format (self.__name)

class Store:
    def __init__ (self): self.__items = {}

    def __iadd__ (self, item):
        item, count = item
        if not item in self.__items: self.__items [item] = 0
        self.__items [item] += count
        return self

    @property
    def items (self): return (item for item in self.__items.items () )

    @property
    def products (self): return ( (item, count) for item, count in self.__items.items () if isinstance (item, Product) )

    @property
    def components (self): return ( (item, count) for item, count in self.__items.items () if isinstance (item, Component) )

    def get_assembly_path (self, product, count):
        store = self.__items.copy()
        if product in store:
            take = min (count, store [product] )
            s_trivial = ('Take {} of {}'.format (take, product) )
            count -= take
            if not count:
                print(s_trivial)
                return
            dict_decr(store, product, take)
            product not in store

        order = {item:count for item in product.components}
        cost, solution = solver(order, store)
        if cost is None:
            print("No solution.")
            return

        print("Solution:")
        print(s_trivial)
        for item, count in solution.items():
            if isinstance(item, Component):
                print ('Take {} of {}'.format (count, item) )
            else:
                assert isinstance(item, Product)
                print ('Disassemble {} of {}'.format (count, item) )

    def getAssemblyPath (self, product, count):
        if product in self.__items:
            take = min (count, self.__items [product] )
            print ('Take {} of {}'.format (take, product) )
            count -= take
            if not count: return
        components = dict ( (comp, count) for comp in product.components)
        for comp, count in self.components:
            if comp not in components: continue
            take = min (count, components [comp] )
            print ('Take {} of {}'.format (take, comp) )
            components [comp] -= take
            if not components [comp]: del components [comp]
            if not components: return
        for prod, count in self.products:
            if prod == product: continue
            shared = set (prod.components) & set (components.keys () )
            dis = min (max (components [comp] for comp in shared), count)
            print ('Disassemble {} of {}.'.format (dis, prod) )
            for comp in shared:
                print ('Take {} of {}.'.format (dis, comp) )
                components [comp] -= take
                if not components [comp]: del components [comp]
                if not components: return
        print ('Missing components:')
        for comp, count in components.items ():
            print ('{} of {}.'.format (count, comp) )

def str_d(d):
    lst = list(d.items())
    lst.sort(key=str)
    return "{" + ", ".join("{}:{}".format(k, v) for (k, v) in lst) + "}"

def dict_incr(d, key, n):
    if key not in d:
        d[key] = n
    else:
        d[key] += n

def dict_decr(d, key, n):
    assert d[key] >= n
    d[key] -= n
    if d[key] == 0:
        del(d[key])

def solver(order, store):
    """
    order is a dict mapping component:count
    store is a dict mapping item:count

    returns a tuple: (cost, solution)
        cost is a cost metric estimating the expense of the solution
        solution is a dict that maps item:count (how to fill the order)

    """
    print("DEBUG: solver: {} {}".format(str_d(order), str_d(store)))
    if not order:
        solution = {}
        cost = 0
        return (cost, solution)

    solutions = []
    for item in store:
        if not isinstance(item, Component):
            continue
        print("...considering: {}".format(item))
        if not item in order:
            continue
        else:
            o = order.copy()
            s = store.copy()
            dict_decr(o, item, 1)
            dict_decr(s, item, 1)
            if not o:
                # we have found a solution!  Return it
                solution = {}
                solution[item] = 1
                cost = 1
                print("BASIS: solver: {} {} / {} {}".format(str_d(order), str_d(store), cost, str_d(solution)))
                return (cost, solution)
            else:
                cost, solution = solver(o, s)
                if cost is None:
                    continue  # this was a dead end
                dict_incr(solution, item, 1)
                cost += 1
                solutions.append((cost, solution))

    for item in store:
        if not isinstance(item, Product):
            continue
        print("...Product components: {} {}".format(item, item.components))
        assert isinstance(item, Product)
        if any(c in order for c in item.components):
            print("...disassembling: {}".format(item))
            o = order.copy()
            s = store.copy()
            dict_decr(s, item, 1)
            for c in item.components:
                dict_incr(s, c, 1)
            cost, solution = solver(o, s)
            if cost is None:
                continue  # this was a dead end
            cost += 1 # cost of disassembly
            solutions.append((cost, solution))
        else:
            print("DEBUG: ignoring {}".format(item))

    if not solutions:
        print("DEBUG: *dead end*")
        return (None, None)
    print("DEBUG: finding min of: {}".format(solutions))
    return min(solutions)



c1 = Component ('alpha')
c2 = Component ('bravo')
c3 = Component ('charlie')
c4 = Component ('delta')

p1 = Product ('A', [c1, c2] )
p2 = Product ('B', [c1, c2, c3] )
p3 = Product ('C', [c1, c3, c4] )

store = Store ()
store += (c2, 100)
store += (c4, 100)
store += (p1, 100)
store += (p2, 100)
store += (p3, 10)
#store.getAssemblyPath (p3, 20)
store.get_assembly_path(p3, 20)

答案 1 :(得分:2)

  1. N个产品的最佳路径< =>单品的最佳路径。
  2. 实际上,如果我们需要最优地组装产品X的N,在我们最佳地(使用当前库存)组装一个产品之后,问题变为使用剩余库存最佳地组装(N-1)产品X.

    =>因此,提供一次最佳组装一个产品X的算法就足够了。

    1. 假设我们需要产品的组件x1,.. xn(这里我们只包含不可用作库存组件的组件)
    2. 对于每个组件xk,查找具有此组件的所有产品。我们将获得每个组件的产品列表 - 产品A1(1),..,A1(i1)具有组件x1,产品A(1),.., A(i2)具有组件x2,依此类推(某些产品可以包含在几个列表A1,A2,..,An列表中)。

      如果任何列表为空 - 没有解决方案。

      我们需要最少的产品组,以便每个产品列表中包含该产品的产品。最简单但计算效率不高的解决方案是蛮力 - 尝试所有集合并选择最小值:

      • 结合A1,..,An - 称之为A(在联盟中仅包含唯一的产品)。

      一个。从A中取出单个产品,如果它包含在所有A1,..,An中 - 我们只需要一次拆卸(本产品)。 湾尝试A中两种产品的所有组合,如果任何组合(a1,a2)满足a1或a2包含在每个列表A1,..,An中的条件 - 它是一种解决方案。

      ...

      当然,深度为n的解决方案 - 每个列表A1,..,An中的一个组件。如果我们之前没有找到解决方案,这是最好的解决方案。

      现在,我们只需要考虑更好的策略然后强力检查,我认为这是可能的 - 我需要考虑它,但这种强力方法肯定会找到严格的最佳解决方案。


      修改

      更准确的解决方案是按长度对列表进行排序。然后在检查K个产品集合作为解决方案时 - 只需要检查来自第一个K列表的每个列表中的1个项目的所有可能组合,如果没有解决方案 - 没有解决问题的最小深度K集合。这种类型的检查在计算上也不会那么糟糕 - 也许它可以起作用????

答案 2 :(得分:0)

我认为这里的关键是确定每个购买案例的潜在成本,以便购买案例的正确组合最佳地最小化成本函数。 (然后它简单地简化为背包问题)

以下内容可能不是最优的,但这是我的意思的一个例子:

1.作为最终产品的任何产品“花费”它的实际成本(以货币计)。

2.任何可以组装成最终产品的组件或产品(给定其他单独的产品/组件)但不需要拆卸成本的实际价格(以货币计)加上小税(tbd)。

3.可以促进最终产品组装但需要拆卸的任何组件或产品都要花费它的货币价格加上对最终产品组装的小税,以及每次拆卸所需的另一笔小税。 (也许与装配税相同?)。

注意:这些“税”将适用于占据相同案例的所有子产品。

...等等其他可能的情况

然后,找到店面可用的组件和产品的所有可能组合,这些组合和产品能够组装到最终产品中。将这些“装配清单”放入由您选择的成本函数确定的成本排序清单中。之后,开始尽可能多地创建第一个(最低成本)“装配清单”(通过检查装配清单中的所有项目是否仍然可用于商店 - 即您已经将它们用于以前的装配)。一旦你无法再创建这种情况,请从列表中弹出它。重复,直到您需要的所有最终产品都“构建”。

注意:每次“组装”最终产品时,您都需要为当前“汇编列表”中的每个产品描述一个全局计数器。

希望这会让讨论朝着正确的方向发展。祝你好运!