8

我有一个包含物品的商店。每个项目要么是一个组件(原子),要么是由各种组件组成的产品(但绝不是两个或多个相同的组件)。

现在,当我想将产品从商店中取出时,有多种情况:

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

到目前为止,您可以在下面看到我的代码(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 动作”是组装或拆卸一个产品的动作,无论涉及多少组件。我正在寻找最少数量的触摸项目,无论它们是否不同。

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

后者更接近最优。

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

该方法getAssemblyPath仅确定如何获取项目的路径。它不接触实际的商店。它在任何时候都不会分配给self.__items。将其视为向商店发出订单的功能,以保留他在(近期)将来必须做的事情,以便从他的商店中取出所需数量的所需物品。

--

编辑2:

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

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

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

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

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

我们有 A 的 0 个,B 的 100 个,C 的 100 个和 D 的 100 个。我们需要 A 的 10 个。现在如果我们首先寻找与 A 共享大多数组件的产品,我们会找到 B。我们拆卸 10 个B 得到 10 个 α、β、δ 和 ε。但是我们需要拆解 C 的 10 个(得到 γ)和 D 的 10 个(得到 ζ)。这将是 40 个动作(30 个拆卸和 10 个组装)。但最好的方法是拆卸 10 个 C 和 10 个 D(30 个动作,20 个拆卸和 10 个组装)。

--

编辑 3:

您无需发布 python 代码即可赢得赏金。只需向我解释该算法并证明它确实会产生最佳路径,或者如果存在多个最佳路径之一。

4

3 回答 3

3

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

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

这是一个基于您的示例的具体示例。我们需要填写由组件 c1、c3 和 c4 组成的产品 3 (p3) 的订单。我们的订单是 20 个 p3,我们有 10 个 p3 库存,所以我们很容易完成前 10 个 p3 的订单。现在我们的订单是 p3 的 10 个订单,但我们可以将其视为 c1 的 10 个订单、c3 的 10 个订单和 c4 的 10 个订单。对于第一个递归调用,我们拆解了一个 p1,并为单个 c1 填写订单并在 store 中放置一个额外的 c2;因此,此递归调用适用于 c1 的 9 个、c3 的 10 个和 c4 的 10 个,并在商店中更新了可用性。对于第二个递归调用,我们拆解了一个 p2,为一个 c1 和一个 c4 填写一个订单,然后将一个额外的 c2 放入 store;因此,此递归调用适用于 c1 的 9 个、c3 的 10 个和 c4 的 9 个,并在商店中更新了可用性。

由于每次调用都会减少问题,因此递归的调用系列将终止。递归调用应该返回一个成本度量,它要么表示调用未能找到解决方案,要么表示找到的解决方案成本有多少;该函数通过选择成本最低的解决方案来选择最佳解决方案。

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

什么是 memoization 以及如何在 Python 中使用它?

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

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

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

此外,此代码拆开产品并将它们作为组件添加到商店中,因此最终解决方案将首先说“拆开 10 个产品 A”之类的内容,然后它会说“拆开 20 个组件 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)
于 2013-03-06T20:03:25.463 回答
2
  1. N 个产品的最优路径 <=> 单个产品的最优路径。

事实上,如果我们需要优化组装 N 个产品 X,在我们优化(使用当前库存)组装一个产品之后,问题就变成了使用剩余库存优化组装 (N-1) 个产品 X。

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

  1. 假设我们需要产品的组件 x1,..xn(这里我们只包括不作为库存组件提供的组件)

对于每个组件 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 集。这种类型的检查在计算上也不会那么糟糕——也许它可以工作????

于 2013-03-06T22:27:34.460 回答
0

I think the key here is to establish the potential costs of each purchase case, so that the proper combination of purchase cases optimally minimize a cost function. (Then its simply reduced to a knapsack problem)

What follows is probably not optimal but here is an example of what I mean:

1.Any product that is the end product "costs" it's actual cost (in currency).

2.Any component or product that can be assembled into the end product (given other separate products/components) but does not require being dissembled costs it's real price (in currency) plus a small tax( tbd).

3.Any component or product that can facilitate assembly of the end product but requires being dissembled costs it's price in currency plus a small tax for the assembly into the end product and another small tax for each dis-assembly needed. (maybe the same value as the assembly tax?).

Note: these "taxes" will apply to all sub-products that occupy the same case.

... and so on for other possible cases

Then, find all possible combinations of components and products available at the storefront that are capable of being assembled into the end product. Place these "assembly lists" into a cost sorted list determined by your chosen cost function. After that, start creating as many of the first (lowest cost) "assembly list" as you can (by checking if all items in assembly list are still available at the store - i.e. you have already used them for a previous assembly). Once you cannot create any more of this case, pop it from the list. Repeat until all the end products you need are "built".

Note: Every time you "assemble" an end product you will need to decriment a global counter for each product in the current "assembly list".

Hope this get's the discussion moving in the right direction. Good luck!

于 2013-02-28T04:40:58.850 回答